Spring Security的默认行为很容易用于标准Web应用程序。它使用基于cookie的身份验证和会话。此外,它会自动为您处理CSRF令牌(防止中间人攻击)。在大多数情况下,您只需要为特定路由设置授权权限,这是通过从数据库中检索用户的方式实现的。
另一方面,如果您只构建一个将与外部服务或SPA /移动应用程序一起使用的REST API,则可能不需要完整会话Session。这是JWT (JSON Web令牌) 一个小型数字签名令牌的用途。所有需要的信息都可以存储在令牌中,因此您的服务器可以实现无会话(no httpsession)。
JWT需要附加到每个HTTP请求,以便服务器可以授权您的用户。有一些选项如何发送令牌。例如,作为URL参数或使用Bearer架构的HTTP Authorization标头:
Authorization: Bearer <token string>
SON Web Token包含三个主要部分:
- 标头 - 通常包括令牌类型和散列算法。
- 有效负载 - 通常包括有关用户的数据以及为其颁发令牌的数据。
- 签名 - 它用于验证消息是否在此过程中未被更改
示例令牌
授权标头中的JWT令牌可能如下所示:
Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJzZWN1cmUtYXBpIiwiYXVkIjoic2VjdXJlLWFwcCIsInN1YiI6InVzZXIiLCJleHAiOjE1NDgyNDI1ODksInJvbCI6WyJST0xFX1VTRVIiXX0.GzUPUWStRofrWI9Ctfv2h-XofGZwcOog9swtuqg1vSkA8kDWLcY3InVgmct7rq4ZU3lxI6CGupNgSazypHoFOA
|
三个部分用逗号分隔 - 标头,声明和签名。标头和有效负载声明是Base64编码的JSON对象。
Header:
{ "typ": "JWT", "alg": "HS512" }
|
有效负载声明:
{ "iss": "secure-api", "aud": "secure-app", "sub": "user", "exp": 1548242589, "rol": [ "ROLE_USER" ] }
|
示例应用
在下面的示例中,我们将创建一个包含2个路由的简单API - 一个公开可用,一个仅授权用户。
我们将使用页面start.spring.io来创建我们的应用程序框架并选择安全性和Web依赖项。其余选项取决于您的喜好。
JWT对Java的支持由库JJWT提供,因此我们还需要将以下依赖项添加到pom.xml文件中:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.10.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.10.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.10.5</version> <scope>runtime</scope> </dependency>
|
控制器
我们的示例应用程序中的控制器将尽可能简单。如果用户未获得授权,他们将只返回消息或HTTP 403错误代码。
@RestController @RequestMapping("/api/public") public class PublicController {
@GetMapping public String getMessage() { return "Hello from public API controller"; } }
|
@RestController @RequestMapping("/api/private") public class PrivateController {
@GetMapping public String getMessage() { return "Hello from private API controller"; } }
|
过滤器
首先,我们将定义一些可重用的常量和默认值,用于生成和验证JWT。
注意:您不应该将JWT签名密钥硬编码到您的应用程序代码中(我们将在示例中暂时忽略它)。您应该使用环境变量或.properties文件。此外,键需要有适当的长度。例如,HS512算法需要密钥,其大小至少为512字节。public final class SecurityConstants {
public static final String AUTH_LOGIN_URL = "/api/authenticate";
// Signing key for HS512 algorithm // You can use the page http://www.allkeysgenerator.com/ to generate all kinds of keys public static final String JWT_SECRET = "n2r5u8x/A%D*G-KaPdSgVkYp3s6v9y$B&E(H+MbQeThWmZq4t7w!z%C*F-J@NcRf";
// JWT token defaults public static final String TOKEN_HEADER = "Authorization"; public static final String TOKEN_PREFIX = "Bearer "; public static final String TOKEN_TYPE = "JWT"; public static final String TOKEN_ISSUER = "secure-api"; public static final String TOKEN_AUDIENCE = "secure-app"; }
|
第一个过滤器将直接用于用户身份验证。它将从URL检查用户名和密码参数,并调用Spring的身份验证管理器来验证它们。
如果用户名和密码正确,则filter将创建一个JWT令牌并在HTTP Authorization标头中返回它。public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager;
setFilterProcessesUrl(SecurityConstants.AUTH_LOGIN_URL); }
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { var username = request.getParameter("username"); var password = request.getParameter("password"); var authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(authenticationToken); }
@Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain, Authentication authentication) { var user = ((User) authentication.getPrincipal());
var roles = user.getAuthorities() .stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList());
var signingKey = SecurityConstants.JWT_SECRET.getBytes();
var token = Jwts.builder() .signWith(Keys.hmacShaKeyFor(signingKey), SignatureAlgorithm.HS512) .setHeaderParam("typ", SecurityConstants.TOKEN_TYPE) .setIssuer(SecurityConstants.TOKEN_ISSUER) .setAudience(SecurityConstants.TOKEN_AUDIENCE) .setSubject(user.getUsername()) .setExpiration(new Date(System.currentTimeMillis() + 864000000)) .claim("rol", roles) .compact();
response.addHeader(SecurityConstants.TOKEN_HEADER, SecurityConstants.TOKEN_PREFIX + token); } }
|
第二个过滤器处理所有HTTP请求,并检查是否存在具有正确令牌的Authorization标头。例如,如果令牌未过期或签名密钥正确。
如果令牌有效,那么过滤器会将身份验证数据添加到Spring的安全上下文中。public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private static final Logger log = LoggerFactory.getLogger(JwtAuthorizationFilter.class);
public JwtAuthorizationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); }
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { var authentication = getAuthentication(request); var header = request.getHeader(SecurityConstants.TOKEN_HEADER);
if (StringUtils.isEmpty(header) || !header.startsWith(SecurityConstants.TOKEN_PREFIX)) { filterChain.doFilter(request, response); return; }
SecurityContextHolder.getContext().setAuthentication(authentication); filterChain.doFilter(request, response); }
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { var token = request.getHeader(SecurityConstants.TOKEN_HEADER); if (StringUtils.isNotEmpty(token)) { try { var signingKey = SecurityConstants.JWT_SECRET.getBytes();
var parsedToken = Jwts.parser() .setSigningKey(signingKey) .parseClaimsJws(token.replace("Bearer ", ""));
var username = parsedToken .getBody() .getSubject();
var authorities = ((List<?>) parsedToken.getBody() .get("rol")).stream() .map(authority -> new SimpleGrantedAuthority((String) authority)) .collect(Collectors.toList());
if (StringUtils.isNotEmpty(username)) { return new UsernamePasswordAuthenticationToken(username, null, authorities); } } catch (ExpiredJwtException exception) { log.warn("Request to parse expired JWT : {} failed : {}", token, exception.getMessage()); } catch (UnsupportedJwtException exception) { log.warn("Request to parse unsupported JWT : {} failed : {}", token, exception.getMessage()); } catch (MalformedJwtException exception) { log.warn("Request to parse invalid JWT : {} failed : {}", token, exception.getMessage()); } catch (SignatureException exception) { log.warn("Request to parse JWT with invalid signature : {} failed : {}", token, exception.getMessage()); } catch (IllegalArgumentException exception) { log.warn("Request to parse empty or null JWT : {} failed : {}", token, exception.getMessage()); } }
return null; } }
|
安全配置
我们需要配置的最后一部分是Spring Security本身。配置很简单,我们需要设置一些细节:
- 密码编码器 - 在我们的例子中是bcrypt
- CORS配置
- 身份验证管理器 - 在我们的例子中简单的内存身份验证,但在现实生活中,你需要像UserDetailsService这样的东西
- 设置哪些端点是安全的以及哪些端点是公开可用的
- 将2个过滤器添加到安全上下文中
- 禁用会话管理 - 我们不需要会话,因此这将阻止会话cookie的创建
@EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true) public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override protected void configure(HttpSecurity http) throws Exception { http.cors().and() .csrf().disable() .authorizeRequests() .antMatchers("/api/public").permitAll() .anyRequest().authenticated() .and() .addFilter(new JwtAuthenticationFilter(authenticationManager())) .addFilter(new JwtAuthorizationFilter(authenticationManager())) .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); }
@Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user") .password(passwordEncoder().encode("password")) .authorities("ROLE_USER"); }
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
@Bean public CorsConfigurationSource corsConfigurationSource() { final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source; } }
|
测试请求公共API:
GET http://localhost:8080/api/public
HTTP/1.1 200 X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY Content-Type: text/plain;charset=UTF-8 Content-Length: 32 Date: Sun, 13 Jan 2019 12:22:14 GMT
Hello from public API controller
Response code: 200; Time: 18ms; Content length: 32 bytes
|
验证用户:
POST http://localhost:8080/api/authenticate?username=user&password=password
HTTP/1.1 200 Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJzZWN1cmUtYXBpIiwiYXVkIjoic2VjdXJlLWFwcCIsInN1YiI6InVzZXIiLCJleHAiOjE1NDgyNDYwNzUsInJvbCI6WyJST0xFX1VTRVIiXX0.yhskhWyi-PgIluYY21rL0saAG92TfTVVVgVT1afWd_NnmOMg__2kK5lcna3lXzYI4-0qi9uGpI6Ul33-b9KTnA X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY Content-Length: 0 Date: Sun, 13 Jan 2019 12:21:15 GMT
<Response body is empty>
Response code: 200; Time: 167ms; Content length: 0 bytes
|
使用令牌请求私有API:
GET http://localhost:8080/api/private Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJzZWN1cmUtYXBpIiwiYXVkIjoic2VjdXJlLWFwcCIsInN1YiI6InVzZXIiLCJleHAiOjE1NDgyNDI1ODksInJvbCI6WyJST0xFX1VTRVIiXX0.GzUPUWStRofrWI9Ctfv2h-XofGZwcOog9swtuqg1vSkA8kDWLcY3InVgmct7rq4ZU3lxI6CGupNgSazypHoFOA
输出:
HTTP/1.1 200 X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY Content-Type: text/plain;charset=UTF-8 Content-Length: 33 Date: Sun, 13 Jan 2019 12:22:48 GMT
Hello from private API controller
Response code: 200; Time: 12ms; Content length: 33 bytes
|
请求没有令牌的私有API:
当您在没有有效JWT的情况下调用安全端点时,您将收到HTTP 403消息。
GET http://localhost:8080/api/private
结论
本文的目的不是展示如何在Spring Security中使用JWT的正确方法。这是一个如何在现实应用程序中执行此操作的示例。
GitHub存储库中找到此示例API的完整源代码。