SpringBoot实现单点登录(SSO)的4种方案
单点登录(Single Sign-On,SSO)是企业应用系统中常见的用户认证方案,它允许用户使用一组凭证访问多个相关但独立的系统,无需重复登录。对于拥有多个应用的企业来说,SSO可以显著提升用户体验并降低凭证管理成本。
一、基于Cookie-Session的传统SSO方案
原理
这是最基础的SSO实现方式,其核心是将用户认证状态存储在服务端Session中,并通过Cookie在客户端保存Session标识符。当用户登录SSO服务器后,服务器创建Session存储用户信息,并将SessionID通过Cookie设置在顶级域名下,使所有子域应用都能访问该Cookie并验证同一个Session。
实现方案
- 创建SSO服务端
@RestController
@RequestMapping("/sso")
public class SsoServerController {
@Autowired
private UserService userService;
@PostMapping("/login")
public ResponseEntity<String> login(
@RequestParam String username,
@RequestParam String password,
HttpServletRequest request,
HttpServletResponse response,
@RequestParam(required = false) String redirect) {
// 验证用户凭证
User user = userService.authenticate(username, password);
if (user == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
}
// 创建session并存储用户信息
HttpSession session = request.getSession(true);
session.setAttribute("USER_INFO", user);
session.setMaxInactiveInterval(3600); // Session有效期1小时
// 获取SessionID
String sessionId = session.getId();
// 设置Cookie
Cookie cookie = new Cookie("SSO_SESSION_ID", sessionId);
cookie.setMaxAge(3600); // 1小时有效期
cookie.setPath("/");
cookie.setDomain(".example.com"); // 关键:顶级域名,使所有子域都能访问
cookie.setHttpOnly(true);
cookie.setSecure(true); // 仅通过HTTPS传输
response.addCookie(cookie);
// 如果有重定向URL,则重定向回原应用
if (redirect != null && !redirect.isEmpty()) {
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", redirect)
.build();
}
return ResponseEntity.ok("Login successful");
}
@GetMapping("/validate")
public ResponseEntity<UserInfo> validateSession(
@CookieValue(name = "SSO_SESSION_ID", required = false) String sessionId) {
if (sessionId == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
// 验证Session有效性并获取用户信息
HttpSession session = sessionRegistry.getSession(sessionId);
if (session == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
// 从Session获取用户信息
User user = (User) session.getAttribute("USER_INFO");
if (user == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
// 返回用户信息
UserInfo userInfo = new UserInfo(user.getId(), user.getUsername(), user.getRoles());
return ResponseEntity.ok(userInfo);
}
@PostMapping("/logout")
public ResponseEntity<Void> logout(
@CookieValue(name = "SSO_SESSION_ID", required = false) String sessionId,
HttpServletResponse response,
@RequestParam(required = false) String redirect) {
// 使Session失效
if (sessionId != null) {
HttpSession session = sessionRegistry.getSession(sessionId);
if (session != null) {
session.invalidate();
}
}
// 删除Cookie
Cookie cookie = new Cookie("SSO_SESSION_ID", null);
cookie.setMaxAge(0);
cookie.setPath("/");
cookie.setDomain(".example.com");
response.addCookie(cookie);
// 如果有重定向URL,则重定向
if (redirect != null && !redirect.isEmpty()) {
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", redirect)
.build();
}
return ResponseEntity.ok().build();
}
}
- 客户端应用集成
@Component
public class SsoFilter implements Filter {
@Autowired
private RestTemplate restTemplate;
private static final String SSO_SERVER_URL = "https://sso.example.com";
private static final String SSO_VALIDATION_URL = SSO_SERVER_URL + "/sso/validate";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 检查当前应用是否已有本地Session
HttpSession currentSession = httpRequest.getSession(false);
if (currentSession != null && currentSession.getAttribute("USER_INFO") != null) {
// 已有本地Session,继续请求
chain.doFilter(request, response);
return;
}
// 获取SSO Session Cookie
Cookie[] cookies = httpRequest.getCookies();
String ssoSessionId = null;
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("SSO_SESSION_ID".equals(cookie.getName())) {
ssoSessionId = cookie.getValue();
break;
}
}
}
// 未找到SSO Cookie,重定向到SSO登录页
if (ssoSessionId == null) {
String redirectUrl = SSO_SERVER_URL + "/login?redirect=" +
URLEncoder.encode(httpRequest.getRequestURL().toString(), "UTF-8");
httpResponse.sendRedirect(redirectUrl);
return;
}
// 验证SSO Session有效性
try {
HttpHeaders headers = new HttpHeaders();
headers.add("Cookie", "SSO_SESSION_ID=" + ssoSessionId);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<UserInfo> responseEntity = restTemplate.exchange(
SSO_VALIDATION_URL, HttpMethod.GET, entity, UserInfo.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
// SSO Session有效,创建本地Session
UserInfo userInfo = responseEntity.getBody();
HttpSession session = httpRequest.getSession(true);
session.setAttribute("USER_INFO", userInfo);
chain.doFilter(request, response);
} else {
// SSO Session无效,重定向到登录页
String redirectUrl = SSO_SERVER_URL + "/login?redirect=" +
URLEncoder.encode(httpRequest.getRequestURL().toString(), "UTF-8");
httpResponse.sendRedirect(redirectUrl);
}
} catch (Exception e) {
// 验证过程出错,重定向到登录页
String redirectUrl = SSO_SERVER_URL + "/login?redirect=" +
URLEncoder.encode(httpRequest.getRequestURL().toString(), "UTF-8");
httpResponse.sendRedirect(redirectUrl);
}
}
}
优缺点
优点:
- 实现相对简单,遵循传统Web开发模式
- 服务端完全控制会话状态和生命周期
- 客户端无需存储和管理复杂状态
- 支持即时会话失效和撤销
缺点:
- 受同源策略限制,仅适用于同一顶级域名下的应用
- 依赖Cookie机制,在某些环境可能受限(如移动应用)
- 存在CSRF风险
二、基于JWT的无状态SSO方案
原理
JWT(JSON Web Token)是一种紧凑的、自包含的令牌格式,可以在不同应用间安全地传递信息。使用JWT实现SSO时,认证服务器在用户登录后生成JWT令牌,其中包含用户相关信息和签名。由于JWT可以独立验证而无需查询中央服务器,它非常适合构建无状态的SSO系统。
实现方案
- 创建JWT认证服务
首先添加依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
实现JWT工具类:
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration}")
private long expirationTime;
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
public String generateToken(User user) {
Map<String, Object> claims = new HashMap<>();
claims.put("id", user.getId());
claims.put("username", user.getUsername());
claims.put("email", user.getEmail());
claims.put("roles", user.getRoles());
return createToken(claims, user.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expirationTime);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), SignatureAlgorithm.HS512)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
public Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
}
}
实现认证控制器:
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private UserService userService;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
// 验证用户凭证
User user = userService.authenticate(
loginRequest.getUsername(),
loginRequest.getPassword()
);
if (user == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("Invalid credentials"));
}
// 生成JWT
String jwt = jwtUtil.generateToken(user);
// 设置刷新令牌(可选)
String refreshToken = UUID.randomUUID().toString();
userService.saveRefreshToken(user.getId(), refreshToken, 30); // 30天有效期
// 返回Token
return ResponseEntity.ok(new JwtResponse(jwt, refreshToken));
}
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request) {
String refreshToken = request.getRefreshToken();
// 验证刷新令牌
User user = userService.findUserByRefreshToken(refreshToken);
if (user == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("Invalid refresh token"));
}
// 生成新的JWT
String newToken = jwtUtil.generateToken(user);
return ResponseEntity.ok(new JwtResponse(newToken, refreshToken));
}
@PostMapping("/validate")
public ResponseEntity<?> validateToken(@RequestBody TokenValidationRequest request) {
String token = request.getToken();
if (!jwtUtil.validateToken(token)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("Invalid token"));
}
// 提取用户信息
Claims claims = jwtUtil.extractAllClaims(token);
Map<String, Object> userInfo = new HashMap<>(claims);
return ResponseEntity.ok(userInfo);
}
@PostMapping("/logout")
public ResponseEntity<?> logout(@RequestBody LogoutRequest request) {
// 删除刷新令牌
userService.removeRefreshToken(request.getRefreshToken());
// JWT本身无法撤销,客户端需要丢弃令牌
return ResponseEntity.ok(new SuccessResponse("Logout successful"));
}
}
- 客户端应用集成
JWT过滤器:
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
// 从Authorization头提取JWT
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
try {
// 验证令牌并提取用户信息
if (jwtUtil.validateToken(jwt)) {
Claims claims = jwtUtil.extractAllClaims(jwt);
username = claims.getSubject();
// 构建认证对象
List<String> roles = (List<String>) claims.get("roles");
List<GrantedAuthority> authorities = roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
UserDetails userDetails = new User(username, "", authorities);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
// JWT验证失败,不设置认证信息
}
}
chain.doFilter(request, response);
}
}
安全配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/public/**", "/auth/login", "/auth/refresh").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
- 前端处理JWT
// 在登录成功后存储JWT令牌
function handleLoginSuccess(response) {
const { token, refreshToken } = response.data;
localStorage.setItem('jwtToken', token);
localStorage.setItem('refreshToken', refreshToken);
// 设置默认Authorization头
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
// 重定向到应用页面
window.location.href = '/dashboard';
}
// 添加请求拦截器自动附加令牌
axios.interceptors.request.use(config => {
const token = localStorage.getItem('jwtToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 添加响应拦截器处理令牌过期
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
// 如果是401错误且不是刷新令牌请求,尝试刷新令牌
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('/auth/refresh', { refreshToken });
const { token } = response.data;
localStorage.setItem('jwtToken', token);
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
return axios(originalRequest);
} catch (err) {
// 刷新失败,重定向到登录页
localStorage.removeItem('jwtToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(err);
}
}
return Promise.reject(error);
}
);
优缺点
优点:
- 完全无状态,服务器不需要存储会话信息
- 跨域支持,适用于分布式系统和微服务
- 可扩展性好,JWT可包含丰富的用户信息
- 不依赖Cookie,避免CSRF问题
- 适用于各种客户端(Web、移动应用、API)
缺点:
- 无法主动失效已颁发的令牌(除非使用黑名单机制)
- JWT可能较大,增加网络传输负担
- 令牌管理需要客户端介入
- 刷新令牌机制较复杂
- 存在令牌被盗用的风险
三、基于OAuth 2.0/OpenID Connect的SSO方案
原理
OAuth 2.0是一个授权框架,而OpenID Connect(OIDC)是建立在OAuth 2.0之上的身份认证层。这是目前最标准化和完善的SSO解决方案,特别适合企业级应用和需要第三方集成的场景。它提供了丰富的授权流程选项和安全特性。
实现方案
在SpringBoot中,可以使用Spring Security OAuth2来实现OAuth 2.0/OIDC服务器和客户端。
- 搭建授权服务器
首先添加依赖:
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.6.8</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
配置授权服务器:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("your-signing-key");
return converter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(
Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.reuseRefreshTokens(false);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource)
.withClient("web-client")
.secret(passwordEncoder.encode("web-client-secret"))
.authorizedGrantTypes("password", "refresh_token", "authorization_code")
.scopes("read", "write")
.redirectUris("http://app1.example.com/login/oauth2/code/custom",
"http://app2.example.com/login/oauth2/code/custom")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(86400)
.and()
.withClient("mobile-client")
.secret(passwordEncoder.encode("mobile-client-secret"))
.authorizedGrantTypes("password", "refresh_token")
.scopes("read")
.accessTokenValiditySeconds(7200)
.refreshTokenValiditySeconds(259200);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
}
// 自定义令牌增强器,添加额外的用户信息
public class CustomTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
if (authentication.getPrincipal() instanceof UserDetails) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Map<String, Object> additionalInfo = new HashMap<>();
// 添加额外的用户信息
if (userDetails instanceof CustomUserDetails) {
CustomUserDetails customUserDetails = (CustomUserDetails) userDetails;
additionalInfo.put("userId", customUserDetails.getId());
additionalInfo.put("email", customUserDetails.getEmail());
additionalInfo.put("fullName", customUserDetails.getFullName());
}
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
}
return accessToken;
}
}
配置资源服务器:
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/user/**").hasRole("USER")
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
}
}
- 客户端应用集成
客户端配置(application.yml):
spring:
security:
oauth2:
client:
registration:
custom:
client-id: web-client
client-secret: web-client-secret
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope: read,write
provider:
custom:
authorization-uri: http://auth-server.example.com/oauth/authorize
token-uri: http://auth-server.example.com/oauth/token
user-info-uri: http://auth-server.example.com/userinfo
jwk-set-uri: http://auth-server.example.com/.well-known/jwks.json
user-name-attribute: sub
客户端安全配置:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/", "/login/**", "/error", "/webjars/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2Login ->
oauth2Login
.loginPage("/login")
.defaultSuccessUrl("/home")
.userInfoEndpoint()
.userService(oAuth2UserService())
)
.logout(logout ->
logout
.logoutSuccessUrl("http://auth-server.example.com/logout?client_id=web-client")
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("JSESSIONID")
);
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService() {
DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
return userRequest -> {
OAuth2User oAuth2User = delegate.loadUser(userRequest);
// 自定义用户信息处理
Map<String, Object> attributes = oAuth2User.getAttributes();
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
return new DefaultOAuth2User(
oAuth2User.getAuthorities(),
attributes,
userNameAttributeName);
};
}
}
- 完整的登出流程
@Controller
public class AuthController {
@GetMapping("/logout")
public String logout(HttpServletRequest request,
HttpServletResponse response,
@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) {
// 清除本地会话
SecurityContextHolder.clearContext();
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
// 清除Cookies
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
cookie.setValue("");
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}
}
// 重定向到OAuth2服务器的注销端点
String logoutUrl = "http://auth-server.example.com/logout?client_id=" +
authorizedClient.getClientRegistration().getClientId() +
"&post_logout_redirect_uri=" +
URLEncoder.encode("http://app.example.com/", StandardCharsets.UTF_8);
return "redirect:" + logoutUrl;
}
}
优缺点
优点:
- 成熟的安全协议,广泛采用的行业标准
- 支持多种认证流程(授权码、隐式、密码等)
- 令牌撤销机制完善
- 可扩展性极好,适合企业级应用
- 明确分离认证与授权职责
缺点:
- 实现复杂度高,小型应用可能不合适
- 配置和理解学习曲线陡峭
四、基于Spring Session的共享会话SSO方案
原理
Spring Session提供了一个将会话数据存储在共享外部存储(如Redis)中的框架,使得不同的应用之间能够共享会话信息。这种方式特别适合基于Spring的同构系统,可以在保持简单实现的同时解决分布式会话共享问题。
实现方案
- 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
- 配置Spring Session
@Configuration
@EnableRedisHttpSession
public class SessionConfig {
@Bean
public LettuceConnectionFactory connectionFactory() {
// Redis连接配置
RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
redisConfig.setHostName("redis-host");
redisConfig.setPort(6379);
return new LettuceConnectionFactory(redisConfig);
}
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("SESSION"); // 使用统一的Cookie名
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\.(\w+\.[a-z]+)$"); // 支持子域
serializer.setUseSecureCookie(true);
serializer.setUseHttpOnlyCookie(true);
return serializer;
}
@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
return new CookieHttpSessionIdResolver();
}
}
- 创建中央认证服务
@Controller
@RequestMapping("/auth")
public class CentralAuthController {
@Autowired
private UserService userService;
@GetMapping("/login")
public String loginPage(@RequestParam(required = false) String redirect, Model model) {
model.addAttribute("redirectUrl", redirect);
return "login";
}
@PostMapping("/login")
public String login(@RequestParam String username,
@RequestParam String password,
@RequestParam(required = false) String redirect,
HttpSession session,
RedirectAttributes redirectAttrs) {
// 验证用户凭证
User user = userService.authenticate(username, password);
if (user == null) {
redirectAttrs.addFlashAttribute("error", "Invalid credentials");
return "redirect:/auth/login";
}
// 将用户信息存入共享会话
UserInfo userInfo = new UserInfo(user.getId(), user.getUsername(), user.getRoles());
session.setAttribute("USER_INFO", userInfo);
// 如果有重定向URL,则重定向回原应用
if (redirect != null && !redirect.isEmpty()) {
return "redirect:" + redirect;
}
return "redirect:/dashboard";
}
@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpSession session) {
// 使会话失效
session.invalidate();
// 可选:获取要重定向的URL
String referer = request.getHeader("Referer");
return "redirect:/auth/login";
}
}
- 创建应用内认证过滤器
@Component
public class SessionAuthenticationFilter implements Filter {
private static final String LOGIN_PAGE = "http://auth.example.com/auth/login";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 公开路径不需要认证
String path = httpRequest.getRequestURI();
if (isPublicPath(path)) {
chain.doFilter(request, response);
return;
}
// 检查session中是否有用户信息
HttpSession session = httpRequest.getSession(false);
boolean authenticated = session != null && session.getAttribute("USER_INFO") != null;
if (authenticated) {
// 用户已认证,继续请求
chain.doFilter(request, response);
} else {
// 未认证,重定向到登录页面
String redirectUrl = LOGIN_PAGE + "?redirect=" +
URLEncoder.encode(httpRequest.getRequestURL().toString(), "UTF-8");
httpResponse.sendRedirect(redirectUrl);
}
}
private boolean isPublicPath(String path) {
return path.startsWith("/public/") ||
path.startsWith("/resources/") ||
path.equals("/error");
}
}
- 配置应用内WebMVC
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private SessionAuthenticationFilter sessionAuthenticationFilter;
@Bean
public FilterRegistrationBean<SessionAuthenticationFilter> sessionFilterRegistration() {
FilterRegistrationBean<SessionAuthenticationFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(sessionAuthenticationFilter);
registration.addUrlPatterns("/*");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); // 在Spring Security之后执行
return registration;
}
}
- 使用会话信息
@Controller
@RequestMapping("/dashboard")
public class DashboardController {
@GetMapping
public String dashboard(HttpSession session, Model model) {
UserInfo userInfo = (UserInfo) session.getAttribute("USER_INFO");
model.addAttribute("user", userInfo);
return "dashboard";
}
}
优缺点
优点:
- 实现简单,易于理解
- 与Spring生态无缝集成
- 会话可包含丰富的信息
缺点:
- 依赖中央存储(如Redis)
- 会话数据需要序列化/反序列化
- 依赖Cookie,不适合非Web应用
五、方案选择与最佳实践
选择建议
方案类型 | 推荐场景 | 不适合场景 |
---|---|---|
Cookie-Session | 同域名下的小型应用,简单的认证需求 | 跨域应用,移动应用集成,高安全性需求 |
JWT | 分布式微服务,前后端分离应用 | 需要即时撤销令牌的场景,极高安全性要求 |
OAuth 2.0/OIDC | 企业级应用,需要第三方集成,多租户系统 | 小型应用,资源受限环境,急速开发需求 |
Spring Session | Spring技术栈的应用,中型企业应用 | 异构技术栈,非Web应用集成 |
六、总结
从简单的基于Cookie-Session的方案,到复杂的OAuth 2.0/OIDC实现,SSO方案的选择应该基于业务需求、安全要求、用户体验目标和技术约束进行综合考量。
本文来自 风象南
下载说明:① 请不要相信网站的任何广告;② 当你使用手机访问网盘时,网盘会诱导你下载他们的APP,大家不要去下载,直接把浏览器改成“电脑模式/PC模式”访问,然后免费普通下载即可;③ 123云盘限制,必须登录后才能下载,且限制每人每天下载流量1GB,下载 123云盘免流量破解工具
版权声明:
小编:吾乐吧
链接:https://wuleba.com/49.html
来源:吾乐吧软件站
本站资源仅供个人学习交流,请于下载后 24 小时内删除,不允许用于商业用途,否则法律问题自行承担。


共有 0 条评论