SSO CAS com segurança de mola
1. Visão geral
Neste artigo, vamos dar uma olhada emintegrating the Central Authentication Service (CAS) with Spring Security. O CAS é um serviço de logon único (SSO).
Digamos que temos aplicativos que exigem autenticação do usuário. O método mais comum é implementar um mecanismo de segurança para cada aplicativo. No entanto, seria melhor implementar a autenticação do usuário para todos os aplicativos em um só lugar.
2. Configuração e instalação do projeto
Existem pelo menos dois componentes envolvidos na configuração de um Serviço de Autenticação Central. Um componente é um servidor baseado em Spring - chamadocas-server. Outros componentes são compostos de um ou mais clientes.
Um cliente pode ser qualquer aplicativo da web que está usando o servidor para autenticação.
2.1. Configuração do servidor CAS
O servidor usa o estilo Overlay de guerra Maven (Gradle) para facilitar a configuração e a implantação. Existe um modelo de início rápido que pode ser clonado e usado.
Vamos clonar:
git clone https://github.com/apereo/cas-overlay-template.git cas-server
Este comando clonacas-overlay-template no diretóriocas-server na máquina local.
A seguir, vamos adicionar dependências adicionais à raizpom.xml. Essas dependências permitem o registro do serviço por meio de uma configuração JSON.
Além disso, eles facilitam as conexões com o banco de dados:
org.apereo.cas
cas-server-support-json-service-registry
${cas.version}
org.apereo.cas
cas-server-support-jdbc
${cas.version}
org.apereo.cas
cas-server-support-jdbc-drivers
${cas.version}
A versão mais recente das dependênciascas-server-support-json-service-registry,cas-server-support-jdbcecas-server-support-jdbc-drivers pode ser encontrada no Maven Central. Observe que o paipom.xml gerencia automaticamente as versões do artefato.
A seguir, vamos criar a pastacas-server/src/main/resourcese copiar a pastacas-server/etc. afim disso. Também vamos mudar a porta do aplicativo, bem como o caminho do armazenamento de chaves SSL.
Nós os configuramos editando as entradas associadas emcas-server/src/main/resources/application.properties:
server.port=6443
server.ssl.key-store=classpath:/etc/cas/thekeystore
standalone.config=classpath:/etc/cas/config
O caminho da pasta de configuração também foi definido comoclasspath:/etc/cas/config. Ele aponta paracas-server/src/main/resources/etc/cas/config.
A próxima etapa é gerar um armazenamento de chaves SSL local. O armazenamento de chaves é usado para estabelecer conexões HTTPS. Esta etapa é importante e não pode ser ignorada.
No terminal, altere o diretório paracas-server/src/main/resources/etc/cas. Depois disso, execute o seguinte comando:
keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore
-storepass changeit -validity 360 -keysize 2048
It’s important to use localhost when prompted for a first and last name, nome da organização e até mesmo unidade organizacional. Não fazer isso pode levar a um erro durante o SSL Handshake. Outros campos como cidade, estado e país podem ser definidos conforme apropriado.
O comando acima gera um armazenamento de chaves com o nomethekeystoree senhachangeit. Ele é armazenado no diretório atual.
Em seguida, o armazenamento de chaves gerado precisa ser exportado para um formato.crt para uso pelos aplicativos cliente. Portanto, ainda no mesmo diretório, execute o seguinte comando para exportar o arquivothekeystore gerado parathekeystore.crt. A senha permanece inalterada:
keytool -export -alias thekeystore -file thekeystore.crt
-keystore thekeystore
Agora, vamos importar othekeystore.crt exportado para o armazenamento de chaves Javacacerts. O prompt do terminal ainda deve estar no diretóriocas-server/src/main/resources/etc/cas.
A partir daí, execute o comando:
keytool -import -alias thekeystore -storepass changeit -file thekeystore.crt
-keystore "C:\Program Files\Java\jdk1.8.0_152\jre\lib\security\cacerts"
Apenas para ter certeza, também podemos importar o certificado para um JRE que está fora da instalação do JDK:
keytool -import -alias thekeystore -storepass changeit -file thekeystore.crt
-keystore "C:\Program Files\Java\jre1.8.0_152\lib\security\cacerts"
Observe que o sinalizador-keystore aponta para a localização do armazenamento de chaves Java na máquina local. Esse local pode ser diferente dependendo da instalação do Java em questão.
Além disso,ensure that the JRE that is referenced as the location of the key store is the same as the one that is used for the client application.
Depois de adicionarthekeystore.crt com sucesso ao armazenamento de chaves Java, precisamos reiniciar o sistema. Equivalentemente, podemos matar todas as instâncias da JVM em execução na máquina local.
Em seguida, do diretório raiz do projeto,cas-server, invoca os comandosbuild packageebuild run do terminal. Iniciar o servidor pode levar algum tempo. Quando estiver pronto, ele imprime READY no console.
Neste ponto, visitarhttps://localhost:6443/cas com um navegador renderiza um formulário de login. O nome de usuário padrão écasusere a senha éMellon.
2.2. Configuração do cliente CAS
Vamos usar oSpring Initializr para gerar o projeto com as seguintes dependências: Web, Security, Freemarker e opcionalmente DevTools.
Além das dependências geradas pelo Spring Initializr, vamos adicionar a dependência para o módulo CAS do Spring Security:
org.springframework.security
spring-security-cas
A versão mais recente da dependência pode ser encontrada emMaven Central. Vamos também configurar a porta do servidor para escutar na porta 9000 adicionando a seguinte entrada emapplication.properties:
server.port=9000
3. Registro de serviços / clientes com o servidor CAS
O servidor não permite que qualquer cliente acesse-o para autenticação. Oclients/services must be registered in the CAS server services registry.
Existem algumas maneiras de registrar um serviço no servidor. Isso inclui YAML, JSON, Mongo, LDAP eothers.
Dependendo do método, existem dependências a serem incluídas no arquivopom.xml. Neste artigo, usamos o método JSON Service Registry. A dependência já foi incluída no arquivopom.xml na seção anterior.
Vamos criar um arquivo JSON que contém a definição do aplicativo cliente. Dentro da pastacas-server/src/main/resources, vamos criar outra pasta -services. É esta pastaservices que contém os arquivos JSON.
A seguir, criamos um arquivo JSON denominadocasSecuredApp-19991.json no diretóriocas-server/src/main/resources/services com o seguinte conteúdo:
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "^http://localhost:9000/login/cas",
"name" : "CAS Spring Secured App",
"description": "This is a Spring App that usses the CAS Server for it's authentication",
"id" : 19991,
"evaluationOrder" : 1
}
O atributoserviceId define um padrão de URL regex para o aplicativo cliente que pretende usar o servidor para autenticação. Nesse caso, o padrão corresponde a um aplicativo em execução no host local e atendendo na porta 9000.
O atributoid deve ser exclusivo para evitar conflitos e configurações de substituição acidentalmente. O nome do arquivo de configuração do serviço segue a convençãoserviceName-id.json. Outros atributos configuráveis, comotheme,proxyPolicy,logo,privacyUrl, e outros podem ser encontradoshere.
Por enquanto, vamos apenas adicionar mais dois itens de configuração para ativar o JSON Service Registry. Uma é informar o servidor no diretório em que os arquivos de configuração do serviço estão localizados. A outra é ativar a inicialização do registro de serviço a partir dos arquivos de configuração JSON.
Ambos os itens de configuração são colocados em outro arquivo, denominadocas.properties.. Criamos este arquivo no diretóriocas-server /src/main/resources:
cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.config.location=classpath:/services
Vamos executar o comandobuild run novamente e anotar as linhas como “Loaded [3] service(s) from [JsonServiceRegistryDao]” no console.
4. Configuração de segurança da primavera
4.1. Configurando o Single Sign-On
Agora que o aplicativo Spring Boot foi registrado no servidor CAS como um serviço. Vamosconfigure Spring Security to work in concert with the server para autenticação do usuário. A sequência completa de interações entre Spring Security e o servidor pode ser encontradahere.
Vamos primeiro configurar os beans que estão relacionados ao módulo CAS do Spring Security. Isso permite que o Spring Security colabore com o serviço de autenticação central.
Para tanto, precisamos adicionar os beans de configuração à classeCasSecuredAppApplication– o ponto de entrada para o aplicativo Spring Boot:
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService("http://localhost:9000/login/cas");
serviceProperties.setSendRenew(false);
return serviceProperties;
}
@Bean
@Primary
public AuthenticationEntryPoint authenticationEntryPoint(
ServiceProperties sP) {
CasAuthenticationEntryPoint entryPoint
= new CasAuthenticationEntryPoint();
entryPoint.setLoginUrl("https://localhost:6443/cas/login");
entryPoint.setServiceProperties(sP);
return entryPoint;
}
@Bean
public TicketValidator ticketValidator() {
return new Cas30ServiceTicketValidator(
"https://localhost:6443/cas");
}
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(serviceProperties());
provider.setTicketValidator(ticketValidator());
provider.setUserDetailsService(
s -> new User("casuser", "Mellon", true, true, true, true,
AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
provider.setKey("CAS_PROVIDER_LOCALHOST_9000");
return provider;
}
Configuramos o beanServiceProperties com a URL de login do serviço padrão para a qual oCasAuthenticationFilter será mapeado internamente. A propriedadesendRenew deServiceProperties é definida comofalse. Como conseqüência, um usuário precisa apresentar credenciais de logon ao servidor apenas uma vez.
A autenticação subsequente será feita automaticamente, ou seja, sem solicitar ao usuário o nome de usuário e a senha novamente. Esse comportamento significa que um único usuário que tem acesso a vários serviços que usam o mesmo servidor para autenticação.
Como veremos mais tarde, se um usuário se desconectar completamente do servidor, seu tíquete será invalidado. Como conseqüência, o usuário é desconectado de todos os aplicativos conectados ao servidor ao mesmo tempo. Isso é chamado de logoff único.
Configuramos o beanAuthenticationEntryPoint com a URL de login padrão do servidor. Observe que esse URL é diferente do URL de login do serviço. Este URL de login do servidor é o local para o qual o usuário será redirecionado para autenticação.
OTicketValidator é o bean que o aplicativo de serviço usa para validar um tíquete de serviço concedido a um usuário após a autenticação bem-sucedida com o servidor.
O fluxo é:
-
Um usuário tenta acessar uma página segura
-
OAuthenticationEntryPoint é acionado e leva o usuário ao servidor. O endereço de login do servidor foi especificado noAuthenticationEntryPoint
-
Em uma autenticação bem-sucedida com o servidor, ele redireciona a solicitação de volta ao URL do serviço que foi especificado, com o ticket de serviço anexado como um parâmetro de consulta
-
CasAuthenticationFilter é mapeado para um URL que corresponde ao padrão e, por sua vez, aciona a validação do tíquete internamente.
-
Se o ticket for válido, um usuário será redirecionado para o URL solicitado originalmente
Agora, precisamos configurar o Spring Security para proteger algumas rotas e usar o beanCasAuthenticationEntryPoint.
Vamos criarSecurityConfig.java que estendeWebSecurityConfigurerAdaptere substituir oconfig():
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.regexMatchers("/secured.*", "/login")
.authenticated()
.and()
.authorizeRequests()
.regexMatchers("/")
.permitAll()
.and()
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint);
}
// ...
}
Além disso, na classeSecurityConfig, substituímos os seguintes métodos e criamos o beanCasAuthenticationFilter ao mesmo tempo:
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private AuthenticationProvider authenticationProvider;
private AuthenticationEntryPoint authenticationEntryPoint;
private SingleSignOutFilter singleSignOutFilter;
private LogoutFilter logoutFilter;
@Autowired
public SecurityConfig(CasAuthenticationProvider casAuthenticationProvider, AuthenticationEntryPoint eP,
LogoutFilter lF
, SingleSignOutFilter ssF
) {
this.authenticationProvider = casAuthenticationProvider;
this.authenticationEntryPoint = eP;
this.logoutFilter = lF;
this.singleSignOutFilter = ssF;
}
// ...
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
}
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return new ProviderManager(Arrays.asList(authenticationProvider));
}
@Bean
public CasAuthenticationFilter casAuthenticationFilter(ServiceProperties sP) throws Exception {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setServiceProperties(sP);
filter.setAuthenticationManager(authenticationManager());
return filter;
}
}
Vamos criar controladores que lidem com solicitações direcionadas a/secured,/logine a página inicial também.
A página inicial é mapeada para umIndexController que tem um métodoindex(). Este método apenas retorna a visualização do índice:
@GetMapping("/")
public String index() {
return "index";
}
O caminho/login é mapeado para o métodologin() da classeAuthController. Ele apenas redireciona para a página de login padrão bem-sucedida.
Observe que ao configurar oHttpSecurity acima, configuramos o caminho/login para que ele exija autenticação. Dessa forma, redirecionamos o usuário para o servidor CAS para autenticação.
Este mecanismo é um pouco diferente da configuração normal, onde o caminho/login não é uma rota protegida e retorna um formulário de login:
@GetMapping("/login")
public String login() {
return "redirect:/secured";
}
O caminho/secured é mapeado para o métodoindex() da classeSecuredPageController. Ele obtém o nome de usuário do usuário autenticado e o exibe como parte da mensagem de boas-vindas:
@GetMapping
public String index(ModelMap modelMap) {
Authentication auth = SecurityContextHolder.getContext()
.getAuthentication();
if(auth != null
&& auth.getPrincipal() != null
&& auth.getPrincipal() instanceof UserDetails) {
modelMap.put("username", ((UserDetails) auth.getPrincipal()).getUsername());
}
return "secure/index";
}
Observe que todas as visualizações estão disponíveis na pastaresources decas-secured-app. Neste ponto, ocas-secured-app deve ser capaz de usar o servidor para autenticação.
Por fim, executamosbuild run no terminal e, simultaneamente, também iniciamos o aplicativo de inicialização Spring. Observe que o SSL é a chave de todo esse processo, portanto, a etapa de geração do SSL acima não deve ser ignorada!
4.2. Configurando Logout Único
Vamos prosseguir comthe authentication process by logging out a user do sistema. Existem dois lugares nos quais um usuário pode ser desconectado: o aplicativo cliente e o servidor.
Desconectar um usuário do aplicativo / serviço do cliente é a primeira coisa a fazer. Isso não afeta o estado de autenticação do usuário em outros aplicativos conectados ao mesmo servidor. Obviamente, desconectar um usuário do servidor também desconecta o usuário de todos os outros serviços / clientes registrados.
Vamos começar definindo algumas configurações de bean na classeCasSecuredAppApplicaiton:
@Bean
public SecurityContextLogoutHandler securityContextLogoutHandler() {
return new SecurityContextLogoutHandler();
}
@Bean
public LogoutFilter logoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter(
"https://localhost:6443/cas/logout",
securityContextLogoutHandler());
logoutFilter.setFilterProcessesUrl("/logout/cas");
return logoutFilter;
}
@Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setCasServerUrlPrefix("https://localhost:6443/cas");
singleSignOutFilter.setIgnoreInitConfiguration(true);
return singleSignOutFilter;
}
@EventListener
public SingleSignOutHttpSessionListener singleSignOutHttpSessionListener(
HttpSessionEvent event) {
return new SingleSignOutHttpSessionListener();
}
Configuramos ologoutFilter para interceptar o padrão de URL/logout/cas e para redirecionar o aplicativo para o servidor para um logout de todo o sistema. O servidor envia uma única solicitação de logout a todos os serviços envolvidos. Essa solicitação é tratada porSingleSignOutFilter,, o que invalida a sessão HTTP.
Vamos modificar a configuração deHttpSecurity na classeconfig() da classeSecurityConfig. OsCasAuthenticationFiltereLogoutFilter que foram configurados anteriormente agora são adicionados à cadeia também:
http
.authorizeRequests()
.regexMatchers("/secured.*", "/login")
.authenticated()
.and()
.authorizeRequests()
.regexMatchers("/")
.permitAll()
.and()
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.logout().logoutSuccessUrl("/logout")
.and()
.addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class)
.addFilterBefore(logoutFilter, LogoutFilter.class);
Para que o logout funcione corretamente, devemos implementar um métodologout() que primeiro desconecte um usuário do sistema localmente e mostre uma página com um link para, opcionalmente, desconectar o usuário de todos os outros serviços conectados ao servidor.
O link é o mesmo definido como a URL do processo de filtro dosLogoutFilter configurados acima:
@GetMapping("/logout")
public String logout(
HttpServletRequest request,
HttpServletResponse response,
SecurityContextLogoutHandler logoutHandler) {
Authentication auth = SecurityContextHolder
.getContext().getAuthentication();
logoutHandler.logout(request, response, auth );
new CookieClearingLogoutHandler(
AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY)
.logout(request, response, auth);
return "auth/logout";
}
A visualização de logout:
Cas Secured App - Logout
You have logged out of Cas Secured Spring Boot App Successfully
Log out of all other Services
5. Conectando o servidor CAS a um banco de dados
Temos usado credenciais de usuário estáticas para autenticação. No entanto, em ambientes de produção, as credenciais do usuário são armazenadas em um banco de dados na maioria das vezes. Então, a seguir,we show how to connect our server to a MySQL database (database name: test) rodando localmente.
Fazemos isso anexando os seguintes dados ao arquivoapplication.properties no diretóriocas-server/src/main/resources:
cas.authn.accept.users=
cas.authn.accept.name=
cas.authn.jdbc.query[0].sql=SELECT * FROM users WHERE email = ?
cas.authn.jdbc.query[0].url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialect
cas.authn.jdbc.query[0].user=root
cas.authn.jdbc.query[0].password=root
cas.authn.jdbc.query[0].ddlAuto=none
cas.authn.jdbc.query[0].driverClass=com.mysql.cj.jdbc.Driver
cas.authn.jdbc.query[0].fieldPassword=password
cas.authn.jdbc.query[0].passwordEncoder.type=NONE
Lembre-se de que o conteúdo completo deapplication.properties pode ser encontrado no código-fonte. Leaving the value of cas.authn.accept.users blank deactivates the use of static user repositories by the server.
Além disso, definimos a instrução SQL que obtém os usuários do banco de dados. A capacidade de configurar o próprio SQL torna o armazenamento de usuários no banco de dados muito flexível.
De acordo com o SQL acima, o registro de um usuário é armazenado na tabelausers. A colunaemail é o que representa o principal do usuário (nome de usuário). Mais abaixo na configuração, definimos o nome do campo de senha,cas.authn.jdbc.query[0].fieldPassword.. Definimos como o valorpassword para aumentar ainda mais a flexibilidade.
Outros atributos que configuramos são o usuário do banco de dados (root) e senha (em branco), dialeto e a conexão JDBCString. A lista de bancos de dados suportados, drivers disponíveis e dialetos pode ser encontradahere .
Another essential attribute is the encryption type used for storing the password. Nesse caso, é definido como NENHUM.
No entanto, o servidor suporta mais mecanismos de criptografia, como Bcrypt. Esses mecanismos de criptografia podem ser encontradoshere, junto com outras propriedades configuráveis.
A execução do servidor (build run) agora permite a autenticação de usuários com credenciais que estão presentes no banco de dados configurado. Note again that the principal in the database that the server uses must be the same as that of the client applications.
Nesse caso, o aplicativo Spring Boot deve ter o mesmo valor ([email protected]) para o principal (username) que o do banco de dados conectado ao servidor.
Vamos então modificar oUserDetails conectado ao beanCasAuthenticationProvider configurado na classeCasSecuredAppApplication do aplicativo Spring Boot:
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(serviceProperties());
provider.setTicketValidator(ticketValidator());
provider.setUserDetailsService((s) -> new User(
"[email protected]", "testU",
true, true, true, true,
AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
provider.setKey("CAS_PROVIDER_LOCALHOST_9000");
return provider;
}
Outra coisa a observar é que, embora oUserDetails receba uma senha, ela não é usada. No entanto, se o nome de usuário for diferente do nome do servidor, a autenticação falhará.
Para que o aplicativo seja autenticado com sucesso com as credenciais armazenadas no banco de dados, inicie um servidor MySQL executando em 127.0.0.1 e porta 3306 com nome de usuário raiz e senha raiz.
Em seguida, use o arquivo SQL,cas-server\src\main esources\create_test_db_and_users_tbl.sql,, que faz parte desource code, para criar a tabelausers no banco de dadostest.
Por padrão, ele contém o e-mail[email protected]e a senhaMellon. Lembre-se, sempre podemos modificar as configurações de conexão do banco de dados emapplication.properties.
Inicie o servidor CAS novamente combuild run, vá parahttps://localhost:6443/case use essas credenciais para autenticação. As mesmas credenciais também funcionarão para o Spring Boot App com proteção de casos.
6. Conclusão
Vimos extensivamente como usar o CAS Server SSO com Spring Security e muitos dos arquivos de configuração envolvidos.
Existem muitos outros aspectos de um servidor que podem ser configurados, variando de temas e tipos de protocolo a políticas de autenticação. Todos eles podem ser encontradoshere nos documentos.