前言
在现代的Web应用程序中,单点登录(Single Sign-On)已经变得越来越流行。单点登录使得用户只需要一次认证即可访问多个应用程序,同时也提高了应用程序的安全性。Spring Boot作为一种广泛使用的Web开发框架,在单点登录方面也提供了很好的支持。
在本文中,我们将使用Spring Boot构建一个基本的单点登录系统。我们将介绍如何使用Spring Security和JSON Web Tokens(JWTs)来实现单点登录功能。本文假设您已经熟悉Spring Boot和Spring Security。
什么是JWT?
在介绍实现单点登录之前,让我们先了解一下JWT。JWT是一种基于JSON格式的开放标准(RFC 7519),用于在不同的应用程序之间安全地传输信息。它由三个部分组成:
- 标头(Header):包含JWT的类型和使用的签名算法。
- 负载(Payload):包含实际的信息。
- 签名(Signature):使用私钥生成的签名,用于验证JWT的真实性。
JWT通常在身份验证过程中使用,以便在不需要存储用户信息的情况下验证用户身份。由于JWT是基于标准化的JSON格式构建的,因此在多种编程语言中都可以轻松地实现和解析。
实现单点登录
下面我们来介绍如何使用JWT实现基本的单点登录系统。这个系统由两个应用程序组成:认证应用程序和资源应用程序。用户在认证应用程序上进行一次身份验证之后,就可以访问资源应用程序。
认证应用程序
我们首先需要构建一个认证应用程序,用于认证用户信息并生成JWT。
添加依赖
首先,我们需要添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- spring-boot-starter-security:用于提供基本的安全性支持。
- jjwt:JSON Web Token的Java实现。
- spring-boot-starter-web:用于提供Web应用程序支持。
配置Spring Security
接下来,我们需要配置Spring Security。我们将使用Spring Security的默认配置,并添加一个自定义的UserDetailsService来从数据库中加载用户信息。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/auth/login")
.successHandler(authenticationSuccessHandler())
.failureHandler(authenticationFailureHandler())
.permitAll()
.and().logout()
.logoutUrl("/auth/logout")
.logoutSuccessUrl("/auth/login?logout")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new JWTAuthenticationSuccessHandler();
}
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new JWTAuthenticationFailureHandler();
}
}
在上述配置中,我们定义了一个路由表达式"/auth/**"允许匿名访问,这意味着认证应用程序的登录和注册页面可以被未经身份验证的用户访问。我们还定义了自定义的AuthenticationSuccessHandler和AuthenticationFailureHandler,用于在用户身份验证成功或失败时生成JWT并将其返回给用户。这些处理程序将在下一步中实现。
实现自定义的AuthenticationSuccessHandler和AuthenticationFailureHandler
在上述配置中,我们使用了自定义的AuthenticationSuccessHandler和AuthenticationFailureHandler。让我们来实现它们。
public class JWTAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private static final String JWT_SECRET = "secret";
private static final long JWT_EXPIRATION_TIME = 864000000; // 10 days
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String username = authentication.getName();
String token = Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, JWT_SECRET.getBytes())
.compact();
response.setHeader("Authorization", "Bearer " + token);
response.getWriter().write("{\"token\":\"Bearer " + token + "\"}");
response.setContentType("application/json");
}
}
在上述代码中,我们使用了JJWT库来生成JWT。在onAuthenticationSuccess方法中,我们首先从Authentication对象获取用户名,然后使用用户名创建JWT。我们设置JWT的有效期为10天,并使用HS512签名算法对JWT进行签名,使用一个字符串作为密钥。最后,我们将JWT作为Bearer令牌添加到响应消息头中,并封装在JSON格式的响应体中返回给客户端。
public class JWTAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\":\"Bad credentials\"}");
response.setContentType("application/json");
}
}
在上述代码中,我们将响应代码设置为401(未经授权),并向响应体中添加一个错误消息,以通知客户端身份验证失败。
实现授权控制器
现在我们已经创建了认证应用程序的基本安全性,让我们来构建资源应用程序并实现授权控制器,以确保只有经过身份验证的用户才可以访问受保护的资源。我们将使用JWT来验证用户身份。
@RestController
public class ResourceController {
private static final String JWT_SECRET = "secret";
@GetMapping("/resource")
public ResponseEntity<String> getResource(HttpServletRequest request) {
String token = request.getHeader("Authorization").replace("Bearer ", "");
if (isValidJWT(token)) {
return ResponseEntity.ok("Protected resource");
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
private boolean isValidJWT(String jwt) {
try {
Jwts.parser().setSigningKey(JWT_SECRET.getBytes()).parseClaimsJws(jwt);
return true;
} catch (JwtException e) {
return false;
}
}
}
在上述代码中,我们使用了一个示例的受保护资源路径"/resource",该路径只允许经过身份验证的用户访问。我们从请求头中提取Bearer令牌,并使用isValidJWT方法验证令牌的真实性。如果JWT有效,则返回200响应代码和受保护的资源;否则返回401(未经授权)响应代码。
资源应用程序
现在我们已经创建了认证应用程序,让我们来创建一个资源应用程序,以便用户可以在验证后访问它。资源应用程序将验证用户是否具有访问受保护资源的权限。
配置Spring Security
我们首先需要配置Spring Security,以使资源应用程序能够验证JWT并授予用户访问权限。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/resource").authenticated()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().addFilterBefore(new JWTAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
在上述配置中,我们定义了一个路由表达式"/resource",只有经过身份验证的用户才能访问。我们还将会话管理策略设置为STATELESS,以避免使用HTTP会话。
实现JWTAuthorizationFilter
接下来,我们需要实现JWTAuthorizationFilter,以验证来自客户端的JWT并将其与用户信息相关联。这将允许我们检查用户是否具有访问资源的权限。
public class JWTAuthorizationFilter extends OncePerRequestFilter {
private static final String JWT_SECRET = "secret";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String jwt = authorizationHeader.replace("Bearer ", "");
try {
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(JWT_SECRET.getBytes()).parseClaimsJws(jwt);
String username = claimsJws.getBody().getSubject();
List<GrantedAuthority> authorities = new ArrayList<>();
UserDetails userDetails = new User(username, "", authorities);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (JwtException e) {
throw new ServletException("Invalid JWT");
}
filterChain.doFilter(request, response);
}
}
在上述代码中,我们查询Authorization头以查找Bearer令牌。如果令牌不存在或不正确,则请求将继续传递。否则,我们使用JJWT库验证JWT的真实性,并获取用户的用户名。我们将用户名创建为Spring Security的UserDetails对象并创建一个UsernamePasswordAuthenticationToken,用于将身份验证信息设置为当前Spring Security上下文的一部分。完成后,请求将继续传递并授权用户访问资源。
测试
现在我们已经创建了认证应用程序和资源应用程序,让我们对它们进行测试。首先,我们在认证应用程序上注册并登录,以获取JWT令牌。然后,我们将使用该令牌访问资源应用程序的受保护资源,并验证我们是否可以成功访问。
# Register and login to authentication application
$ curl -s -X POST -H "Content-Type: application/json" -d '{"username":"user","password":"password"}' http://localhost:8080/auth/signup
$ curl -s -X POST -H "Content-Type: application/json" -d '{"username":"user","password":"password"}' http://localhost:8080/auth/login
# Get JWT token
$ TOKEN=$(curl -si -X POST -H "Content-Type: application/json" -d '{"username":"user","password":"password"}' http://localhost:8080/auth/login | grep 'Authorization:' | awk '{print $2}')
# Access protected resource in resource application
$ curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8090/resource
Protected resource
通过测试,我们可以看到我们成功地访问了资源应用程序的受保护资源,并返回了正确的响应。
总结
在本文中,我们使用Spring Boot,Spring Security和JWT实现了一个基本的单点登录系统。我们介绍了JWT的概念,并演示了如何使用它来验证用户身份。我们还创建了一个认证应用程序和一个资源应用程序,以演示如何在多个应用程序之间共享用户身份验证信息。您可以将此范例用作基础模板,进一步扩展它以适应自己的应用程序需求。