单点登录(Single Sign-On, SSO)是一种身份验证机制,允许用户使用一组凭证(如用户名和密码)登录多个相关但独立的系统。
SSO的核心原理使集中认证、分散授权,主要流程如下:
1.用户访问应用A
2.应用A检查本地会话,发现未登录
3.重定向到SSO认证中心
4.用户在认证中心登录
5.认证中心创建全局会话,并颁发令牌
6.用户携带令牌返回应用A
7.应用A向认证中心验证令牌
8.认证中心返回用户信息,应用A创建本地会话
9.用户访问应用B时重复2-8流程(但无需重复登录)
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-oauth2-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-oauth2-resource-serverartifactId>
dependency>
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client1")
.secret(passwordEncoder().encode("secret1"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("read", "write")
.redirectUris("http://localhost:8081/login/oauth2/code/client1")
.autoApprove(true);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll();
}
}
# application.yml
spring:
security:
oauth2:
client:
registration:
sso:
client-id: client1
client-secret: secret1
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope: read,write
provider:
sso:
issuer-uri: http://localhost:8080
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
public class JwtTokenUtil {
private static final String SECRET = "your-secret-key";
private static final long EXPIRATION = 86400000; // 24小时
public static String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public static String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String token = resolveToken(request);
if (token != null && validateToken(token)) {
String username = JwtTokenUtil.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
<dependency>
<groupId>org.jasig.cas.clientgroupId>
<artifactId>cas-client-support-springbootartifactId>
<version>3.6.4version>
dependency>
2.CAS配置
@Configuration
public class CasConfig {
@Value("${cas.server.url}")
private String casServerUrl;
@Value("${cas.service.url}")
private String serviceUrl;
@Bean
public FilterRegistrationBean<AuthenticationFilter> casAuthenticationFilter() {
FilterRegistrationBean<AuthenticationFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new AuthenticationFilter());
registration.addInitParameter("casServerLoginUrl", casServerUrl + "/login");
registration.addInitParameter("serverName", serviceUrl);
registration.addUrlPatterns("/*");
return registration;
}
@Bean
public ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> casSingleSignOutListener() {
return new ServletListenerRegistrationBean<>(new SingleSignOutHttpSessionListener());
}
}
分布式会话:使用Redis存储会话信息
@Bean
public RedisIndexedSessionRepository sessionRepository(RedisOperations<String, Object> redisOperations) {
return new RedisIndexedSessionRepository(redisOperations);
}
Session共享配置
spring:
session:
store-type: redis
redis:
flush-mode: on_save
namespace: spring:session
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/login", "/oauth/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID");
}
}
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
OAuth2 | 标准协议,安全性高,扩展性强 | 实现复杂度较高 | 企业级应用,多平台集成 |
JWT | 无状态,性能好,适合分布式系统 | 令牌无法主动失效 | 微服务架构,前后端分离 |
CAS | 专为SSO设计,功能完善 | 需要额外部署CAS服务器 | 传统企业应用,教育系统 |
设置正确的Cookie域和路径
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
return serializer;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/mfa-verify").hasRole("PRE_AUTH")
.anyRequest().fullyAuthenticated()
.and()
.formLogin()
.loginPage("/login")
.successHandler((request, response, authentication) -> {
if (needsMfa(authentication)) {
response.sendRedirect("/mfa-verify");
} else {
response.sendRedirect("/home");
}
});
}