Java 单点登录 (SSO) 有以下几种解决方案:
- 基于令牌(Token)的 SSO:用户在登录成功后,得到一个令牌,然后该令牌被用于访问其他相互信任的应用程序。Shiro 和 Spring Security 都支持基于令牌的 SSO。
- 基于代理(Proxy)的 SSO:一个代理服务器接收来自用户的请求,并将其重定向到适当的应用程序,并将用户身份验证信息传递给该应用程序。常见的代理服务器包括 Apache HTTP Server、Nginx 等。
- 基于中央认证授权(Central Authentication Authorization, CAS) 的 SSO:CAS 是一种开源的单点登录协议,它使用令牌来实现 SSO,可以与各种应用程序进行集成。CAS 服务器负责验证用户身份并生成令牌,应用程序则只需检查令牌即可。
- 基于安全标记(Security Assertion Markup Language, SAML) 的 SSO:SAML 是一种 XML 标记语言,用于在不同的应用程序之间交换用户身份验证和授权数据。当用户在一个应用程序上进行身份验证时,SAML 将会生成一个安全标记,并将其发送到其他应用程序以完成 SSO。
- OAuth 和 OpenID Connect:OAuth 用于授权,OpenID Connect 则用于身份验证。它们都是基于令牌的 SSO 解决方案,被广泛应用于第三方身份验证和授权领域。
以下是一些使用 Java 编写基于 Spring Security 的简单 SSO 示例
基于 Cookie 的 SSO
// 配置认证中心
@Configuration
@EnableWebSecurity
public class AuthServerConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login")
.and()
.logout().logoutSuccessUrl("/login").invalidateHttpSession(true)
.deleteCookies("JSESSIONID", "MYAPPID");
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
// 配置子应用
@Configuration
public class SubAppConfig {
// ...
@RequestMapping("/index")
public String index() {
return "index";
}
@RequestMapping("/sub/login")
public String subLogin(HttpServletRequest request, HttpServletResponse response) {
// 检查是否已经登录,如果已经登录就不需要再次重定向到认证中心了
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if ("MYAPPID".equals(cookie.getName())) {
return "index";
}
}
}
// 未登录则跳转到认证中心进行认证
response.sendRedirect("http://authserver.com/login?redirect_uri=http://subapp.com/sub/login/callback");
return null;
}
@RequestMapping("/sub/login/callback")
public String subLoginCallback(HttpServletRequest request, HttpServletResponse response) {
// 认证中心认证成功后会重定向到此接口,携带认证令牌
String token = request.getParameter("token");
// 将认证令牌保存为 Cookie,并跳转回子应用首页
Cookie cookie = new Cookie("MYAPPID", token);
cookie.setPath("/");
cookie.setMaxAge(24 * 60 * 60);
response.addCookie(cookie);
return "index";
}
}
基于JWT的SSO
// 配置认证中心
@Configuration
@EnableWebSecurity
public class AuthServerConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login")
.and()
.logout().logoutSuccessUrl("/login").invalidateHttpSession(true)
.deleteCookies("JSESSIONID");
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
// 配置子应用
@Configuration
public class SubAppConfig {
@Autowired
private RestTemplate restTemplate;
@Value("${authserver.url:http://authserver.com}")
private String authServerUrl;
@Value("${jwt.secret}")
private String jwtSecret;
@RequestMapping("/index")
public String index(HttpServletRequest request) {
String token = getTokenFromCookie(request);
String username = getUsernameFromToken(token);
// ...
}
@RequestMapping("/sub/login")
public String subLogin(HttpServletRequest request, HttpServletResponse response) {
// 检查是否已经登录,如果已经登录就不需要再次重定向到认证中心了
String token = getTokenFromCookie(request);
if (StringUtils.isNotEmpty(token)) {
return "index";
}
// 未登录则跳转到认证中心进行认证
response.sendRedirect(authServerUrl + "/login?redirect_uri=http://subapp.com/sub/login/callback");
return null;
}
@RequestMapping("/sub/login/callback")
public String subLoginCallback(HttpServletRequest request, HttpServletResponse response) {
// 认证中心认证成功后会重定向到此接口,携带 JWT
String jwt = request.getParameter("jwt");
// 验证 JWT 签名,并解析出用户信息
Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(jwt).