在Web应用程序中,认证是保护资源免受未授权访问的重要组成部分。传统的Session认证方式在分布式环境下存在一些问题,而JWT(JSON Web Token)则提供了一种更安全、可扩展、无状态的认证解决方案。
JWT Token简介
JWT是一种基于JSON的开放标准(RFC 7519),用于在各方之间安全地传输信息,该信息可以被验证和信任。它由三部分组成:Header、Payload和Signature。
- Header:包含算法和令牌的类型信息。
- Payload:包含声明(claims)和附加数据。
- Signature:使用Header和Payload以及密钥生成的加密签名。
当用户登录成功后,服务端会生成一个包含用户信息(如用户名、用户角色等)的JWT Token,并将其返回给客户端。接下来,客户端在每次请求时都携带该Token,并将其放在请求的头部或参数中,服务端会通过验证Token的合法性来判断用户是否有权限访问请求的资源。
Spring Boot中使用JWT Token认证
下面我们将使用Spring Boot和Spring Security来实现JWT Token认证。
首先,我们需要添加以下依赖到项目的pom.xml
文件中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
接下来,创建一个JwtTokenUtil
工具类,用于生成和解析JWT Token。具体实现如下:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("sub", userDetails.getUsername());
claims.put("iat", new Date());
return Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUsernameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
Date expirationDate = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody().getExpiration();
return expirationDate.before(new Date());
}
}
在上面的代码中,我们使用io.jsonwebtoken
库来操作JWT Token。JwtTokenUtil
类中的generateToken
方法用于生成Token,getUsernameFromToken
方法用于从Token中获取用户名,validateToken
方法用于验证Token的合法性。
接下来,我们需要创建一个JwtUserDetailsService
类,实现Spring Security的UserDetailsService
接口,用于处理用户认证逻辑。具体实现如下:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class JwtUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if ("admin".equals(username)) {
return new User("admin", passwordEncoder.encode("admin123"), new ArrayList<>());
} else {
throw new UsernameNotFoundException("User not found with username: " + username);
}
}
}
在上面的代码中,我们对Spring Security的UserDetailsService
接口进行了实现,根据用户名查询用户信息。在这里,我们简单地返回一个硬编码的用户名和加密后的密码。
接下来,我们需要创建一个JwtAuthenticationFilter
类,继承自Spring Security的UsernamePasswordAuthenticationFilter
类,用于拦截登录请求并生成Token。具体实现如下:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private JwtTokenUtil jwtTokenUtil;
private JwtUserDetailsService jwtUserDetailsService;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil, JwtUserDetailsService jwtUserDetailsService) {
this.authenticationManager = authenticationManager;
this.jwtTokenUtil = jwtTokenUtil;
this.jwtUserDetailsService = jwtUserDetailsService;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
try {
UserCredentials credentials = new ObjectMapper().readValue(request.getInputStream(), UserCredentials.class);
UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(credentials.getUsername());
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(credentials.getUsername(), credentials.getPassword(), new ArrayList<>()));
} catch (IOException e) {
throw new AuthenticationServiceException("Failed to authenticate user");
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
String token = jwtTokenUtil.generateToken((UserDetails) authResult.getPrincipal());
response.addHeader("Authorization", "Bearer " + token);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(failed.getMessage());
}
private static class UserCredentials {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
}
在上面的代码中,我们在attemptAuthentication
方法中,根据请求参数中的用户名和密码,调用jwtUserDetailsService
查询用户信息,并使用authenticationManager
进行认证。在successfulAuthentication
方法中,我们使用jwtTokenUtil
生成Token,并将其添加到响应头中。
最后,我们需要配置Spring Security,使其使用我们自定义的JwtAuthenticationFilter
。在WebSecurityConfigurerAdapter
的子类中进行以下配置:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtUserDetailsService jwtUserDetailsService;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
在上面的配置中,我们将jwtAuthenticationFilter
添加到了UsernamePasswordAuthenticationFilter
之前,以便在请求到达之前进行Token认证。
总结
通过上面的实现,我们成功地在Spring Boot中使用JWT Token实现了Token认证。JWT Token可以提供更安全、可扩展、无状态的认证解决方案,并且在分布式环境下表现优秀,可以应用于各种类型的Web应用程序中。
更多关于JWT Token的详细信息请参考官方文档。
本文来自极简博客,作者:狂野之心,转载请注明原文链接:Spring Boot中使用JWT Token进行Token认证