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:
{
<font>"typ"</font><font>: </font><font>"JWT"</font><font>,
</font><font>"alg"</font><font>: </font><font>"HS512"</font><font>
}
</font>
有效负载声明:
{
<font>"iss"</font><font>: </font><font>"secure-api"</font><font>,
</font><font>"aud"</font><font>: </font><font>"secure-app"</font><font>,
</font><font>"sub"</font><font>: </font><font>"user"</font><font>,
</font><font>"exp"</font><font>: 1548242589,
</font><font>"rol"</font><font>: [
</font><font>"ROLE_USER"</font><font>
]
}
</font>
示例应用
在下面的示例中,我们将创建一个包含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(<font>"/api/public"</font><font>)
<b>public</b> <b>class</b> PublicController {
@GetMapping
<b>public</b> String getMessage() {
<b>return</b> </font><font>"Hello from public API controller"</font><font>;
}
}
</font>
@RestController
@RequestMapping(<font>"/api/private"</font><font>)
<b>public</b> <b>class</b> PrivateController {
@GetMapping
<b>public</b> String getMessage() {
<b>return</b> </font><font>"Hello from private API controller"</font><font>;
}
}
</font>
过滤器
首先,我们将定义一些可重用的常量和默认值,用于生成和验证JWT。
注意:您不应该将JWT签名密钥硬编码到您的应用程序代码中(我们将在示例中暂时忽略它)。您应该使用环境变量或.properties文件。此外,键需要有适当的长度。例如,HS512算法需要密钥,其大小至少为512字节。
<b>public</b> <b>final</b> <b>class</b> SecurityConstants {
<b>public</b> <b>static</b> <b>final</b> String AUTH_LOGIN_URL = <font>"/api/authenticate"</font><font>;
</font><font><i>// Signing key for HS512 algorithm</i></font><font>
</font><font><i>// You can use the page http://www.allkeysgenerator.com/ to generate all kinds of keys</i></font><font>
<b>public</b> <b>static</b> <b>final</b> String JWT_SECRET = </font><font>"n2r5u8x/A%D*G-KaPdSgVkYp3s6v9y$B&E(H+MbQeThWmZq4t7w!z%C*F-J@NcRf"</font><font>;
</font><font><i>// JWT token defaults</i></font><font>
<b>public</b> <b>static</b> <b>final</b> String TOKEN_HEADER = </font><font>"Authorization"</font><font>;
<b>public</b> <b>static</b> <b>final</b> String TOKEN_PREFIX = </font><font>"Bearer "</font><font>;
<b>public</b> <b>static</b> <b>final</b> String TOKEN_TYPE = </font><font>"JWT"</font><font>;
<b>public</b> <b>static</b> <b>final</b> String TOKEN_ISSUER = </font><font>"secure-api"</font><font>;
<b>public</b> <b>static</b> <b>final</b> String TOKEN_AUDIENCE = </font><font>"secure-app"</font><font>;
}
</font>
第一个过滤器将直接用于用户身份验证。它将从URL检查用户名和密码参数,并调用Spring的身份验证管理器来验证它们。
如果用户名和密码正确,则filter将创建一个JWT令牌并在HTTP Authorization标头中返回它。
<b>public</b> <b>class</b> JwtAuthenticationFilter <b>extends</b> UsernamePasswordAuthenticationFilter {
<b>private</b> <b>final</b> AuthenticationManager authenticationManager;
<b>public</b> JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
<b>this</b>.authenticationManager = authenticationManager;
setFilterProcessesUrl(SecurityConstants.AUTH_LOGIN_URL);
}
@Override
<b>public</b> Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
<b>var</b> username = request.getParameter(<font>"username"</font><font>);
<b>var</b> password = request.getParameter(</font><font>"password"</font><font>);
<b>var</b> authenticationToken = <b>new</b> UsernamePasswordAuthenticationToken(username, password);
<b>return</b> authenticationManager.authenticate(authenticationToken);
}
@Override
<b>protected</b> <b>void</b> successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain, Authentication authentication) {
<b>var</b> user = ((User) authentication.getPrincipal());
<b>var</b> roles = user.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
<b>var</b> signingKey = SecurityConstants.JWT_SECRET.getBytes();
<b>var</b> token = Jwts.builder()
.signWith(Keys.hmacShaKeyFor(signingKey), SignatureAlgorithm.HS512)
.setHeaderParam(</font><font>"typ"</font><font>, SecurityConstants.TOKEN_TYPE)
.setIssuer(SecurityConstants.TOKEN_ISSUER)
.setAudience(SecurityConstants.TOKEN_AUDIENCE)
.setSubject(user.getUsername())
.setExpiration(<b>new</b> Date(System.currentTimeMillis() + 864000000))
.claim(</font><font>"rol"</font><font>, roles)
.compact();
response.addHeader(SecurityConstants.TOKEN_HEADER, SecurityConstants.TOKEN_PREFIX + token);
}
}
</font>
第二个过滤器处理所有HTTP请求,并检查是否存在具有正确令牌的Authorization标头。例如,如果令牌未过期或签名密钥正确。
如果令牌有效,那么过滤器会将身份验证数据添加到Spring的安全上下文中。
<b>public</b> <b>class</b> JwtAuthorizationFilter <b>extends</b> BasicAuthenticationFilter {
<b>private</b> <b>static</b> <b>final</b> Logger log = LoggerFactory.getLogger(JwtAuthorizationFilter.<b>class</b>);
<b>public</b> JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
<b>super</b>(authenticationManager);
}
@Override
<b>protected</b> <b>void</b> doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
<b>var</b> authentication = getAuthentication(request);
<b>var</b> header = request.getHeader(SecurityConstants.TOKEN_HEADER);
<b>if</b> (StringUtils.isEmpty(header) || !header.startsWith(SecurityConstants.TOKEN_PREFIX)) {
filterChain.doFilter(request, response);
<b>return</b>;
}
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
<b>private</b> UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
<b>var</b> token = request.getHeader(SecurityConstants.TOKEN_HEADER);
<b>if</b> (StringUtils.isNotEmpty(token)) {
<b>try</b> {
<b>var</b> signingKey = SecurityConstants.JWT_SECRET.getBytes();
<b>var</b> parsedToken = Jwts.parser()
.setSigningKey(signingKey)
.parseClaimsJws(token.replace(<font>"Bearer "</font><font>, </font><font>""</font><font>));
<b>var</b> username = parsedToken
.getBody()
.getSubject();
<b>var</b> authorities = ((List<?>) parsedToken.getBody()
.get(</font><font>"rol"</font><font>)).stream()
.map(authority -> <b>new</b> SimpleGrantedAuthority((String) authority))
.collect(Collectors.toList());
<b>if</b> (StringUtils.isNotEmpty(username)) {
<b>return</b> <b>new</b> UsernamePasswordAuthenticationToken(username, <b>null</b>, authorities);
}
} <b>catch</b> (ExpiredJwtException exception) {
log.warn(</font><font>"Request to parse expired JWT : {} failed : {}"</font><font>, token, exception.getMessage());
} <b>catch</b> (UnsupportedJwtException exception) {
log.warn(</font><font>"Request to parse unsupported JWT : {} failed : {}"</font><font>, token, exception.getMessage());
} <b>catch</b> (MalformedJwtException exception) {
log.warn(</font><font>"Request to parse invalid JWT : {} failed : {}"</font><font>, token, exception.getMessage());
} <b>catch</b> (SignatureException exception) {
log.warn(</font><font>"Request to parse JWT with invalid signature : {} failed : {}"</font><font>, token, exception.getMessage());
} <b>catch</b> (IllegalArgumentException exception) {
log.warn(</font><font>"Request to parse empty or null JWT : {} failed : {}"</font><font>, token, exception.getMessage());
}
}
<b>return</b> <b>null</b>;
}
}
</font>
安全配置
我们需要配置的最后一部分是Spring Security本身。配置很简单,我们需要设置一些细节:
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = <b>true</b>)
<b>public</b> <b>class</b> SecurityConfiguration <b>extends</b> WebSecurityConfigurerAdapter {
@Override
<b>protected</b> <b>void</b> configure(HttpSecurity http) throws Exception {
http.cors().and()
.csrf().disable()
.authorizeRequests()
.antMatchers(<font>"/api/public"</font><font>).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(<b>new</b> JwtAuthenticationFilter(authenticationManager()))
.addFilter(<b>new</b> JwtAuthorizationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
<b>public</b> <b>void</b> configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser(</font><font>"user"</font><font>)
.password(passwordEncoder().encode(</font><font>"password"</font><font>))
.authorities(</font><font>"ROLE_USER"</font><font>);
}
@Bean
<b>public</b> PasswordEncoder passwordEncoder() {
<b>return</b> <b>new</b> BCryptPasswordEncoder();
}
@Bean
<b>public</b> CorsConfigurationSource corsConfigurationSource() {
<b>final</b> UrlBasedCorsConfigurationSource source = <b>new</b> UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration(</font><font>"/**"</font><font>, <b>new</b> CorsConfiguration().applyPermitDefaultValues());
<b>return</b> source;
}
}
</font>
测试
请求公共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 <b>public</b> 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 <b>private</b> 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的完整源代码。