共计 10658 个字符,预计需要花费 27 分钟才能阅读完成。
引言
最近面试过程中,无心中跟候选人聊到了 JWT 相干的货色,也就联想到我本人对于 JWT 落地过的那些我的项目。
对于 JWT,能够说是分布式系统下的一个利器,我在我的很多我的项目实际中,认证零碎的第一抉择都是 JWT。它的劣势会让你骑虎难下,就像你领优惠券一样。
大家回顾一下一个场景,如果你和你的女朋友想吃某江家的烤鱼了,你会怎么做呢?
传统的时代,我想场景是这样的:咱们走进一家某江家餐厅,会被服务员疏导一个桌子,而后咱们开始点餐,服务原会记录咱们点餐信息,而后在送到后厨去。这个过程中,那个餐桌就相当于 session,而咱们的点餐信息回记录到这个 session 之中,而后送到后厨。这个是一个典型的基于 session 的认证过程。但咱们也发现了它的弊病,就是基于 session 的这种认证,对服务器强依赖,而且信息都是存储在服务器之上,灵活性和扩展性大大降低。
而互联网时代,公众点评、美团、饿了么给了咱们另一个抉择,咱们可能第一工夫会在这些平台上搜寻江边城外的优惠券,这个优惠券中可能会形容着两人实惠套餐明细。这张优惠券就是咱们的 JWT,咱们能够在任何一家有参加优惠活动的餐厅应用这张优惠券,而不用被限度在同一家餐厅。同时这张优惠券中间接记录了咱们的点餐明细,等咱们到了餐厅,只须要将优惠券二维码告知服务员,服务员就会给咱们端上咱们想要的食物。
好了,以上只是一个小例子,其实只是想阐明一下 JWT 相较于传统的基于 session 的认证框架的劣势。
JWT 的劣势在于它能够跨域、跨服务器应用,而 Session 则只能在本域名下应用。而且,JWT 不须要在服务端保留用户的信息,只须要在客户端保留即可,这加重了服务端的累赘。 这一点在分布式架构下劣势还是很显著的。
什么是 JWT
说了这么多,如何定义 JWT 呢?
JWT(JSON Web Token)是一种用于在网络应用中进行身份验证的凋谢规范(RFC7519)。它能够平安地在用户和服务器之间传输信息,因为它应用数字签名来验证数据的完整性和真实性。
JWT 蕴含三个局部:头部、载荷和签名 。头部蕴含算法和类型信息,载荷蕴含用户的信息,签名用于验证数据的完整性和真实性。
额定说一下 poload,也就是负荷局部,这块是 jwt 的外围模块,它外部包含一些申明(claims)。申明由三个类型组成:
Registered Claims:这是预约义的申明名称,次要包含以下几种:
- iss:Token 发行者
- sub:Token 主题
- aud:Token 的受众
- exp:Token 过期工夫
- iat:Token 发行工夫
- jti:Token 惟一标识符
Public Claims:公共申明是本人定义的申明名称,以防止抵触。
Private Claims:公有申明与公共申明相似,不同之处在于它是用于在单方之间共享信息的。
当用户登录时,服务器将生成一个 JWT,并将其作为响应返回给客户端。客户端将在后续的申请中发送此 JWT。服务器将应用雷同的密钥验证 JWT 的签名,并从载荷中获取用户信息。如果签名验证通过并且用户信息无效,则服务器将容许申请持续进行。
JWT 长处
JWT 长处如果咱们零碎的总结一下,如下:
- 跨语言和平台:JWT 是基于 JSON 规范的,因而能够在不同的编程语言和平台之间进行替换和应用。无状态:因为 JWT 蕴含所有必要的信息,服务器不须要在每个申请中存储任何会话数据,因而能够轻松地进行负载平衡。
- 安全性:JWT 应用数字签名来验证数据的完整性和真实性,因而能够避免数据被篡改或伪造。
- 可扩展性:JWT 能够蕴含任何用户信息,因而能够轻松地扩大到其余应用程序中。
- 一个基于 JWT 认证的计划
我将举一个我理论业务落地的一个例子。
我的业务场景中个别都会有一个业务网关,该网关的外围性能就是鉴权和上线文转换。用户申请会将 JWT 字符串存与 header 之中,而后到网关后进行 JWT 解析,解析后的上下文信息,会转变成明文 K - V 的形式在此存于 header 之中,供零碎外部各个微服务之间相互调用时提供明文上下文信息。具体时序图如下:
基于 Spring security 的 JWT 实际
JWT 原理很简略,当然,你能够齐全本人实现 JWT 的全流程,然而,理论中,咱们个别不须要这么干,因为有很多成熟和好用的轮子提供给咱们,而且封装性和安全性也远比本人匆忙的封装一个简略的 JWT 来的高。
如果是基于学习 JWT,我是倡议大家本人手写一个 demo 的,然而如果重实际的角度触发,咱们齐全能够应用 Spring Security 提供的 JWT 组件,来高效疾速的实现一个稳定性和安全性都十分高的 JWT 认证框架。
以下是我基于我的业务理论状况,依据保密性要求,简化了的 JWT 实际代码。也算是抛砖引玉,心愿能够给大家在业务场景中使用 JWT 做一个参考 。
maven 依赖
首先,咱们须要增加以下依赖到 pom.xml 文件中:
<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>
JWT 工具类封装
而后,咱们能够创立一个 JwtTokenUtil 类来生成和验证 JWT 令牌:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtTokenUtil {
private static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
@Value("${jwt.secret}")
private String secret;
public String generateToken(UserDetails userDetails) {Map<String, Object> claims = newHashMap <>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {Date now = new Date();
Date expiration = new Date(now.getTime() + JWT_TOKEN_VALIDITY * 1000);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();}
public boolean validateToken(String token, UserDetails userDetails) {final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {return extractExpiration(token).before(new Date());
}
public String extractUsername(String token) {return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {return extractClaim(token, Claims::getExpiration);
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}
}
在这个实现中,咱们应用了 jjwt 库来创立和解析 JWT 令牌。咱们定义了以下办法:
- generateToken:生成 JWT 令牌。
- createToken:创立 JWT 令牌。
- validateToken:验证 JWT 令牌是否无效。
- isTokenExpired:查看 JWT 令牌是否过期。
- extractUsername:从 JWT 令牌中提取用户名。
- extractExpiration:从 JWT 令牌中提取过期工夫。
- extractClaim:从 JWT 令牌中提取指定的申明。
- extractAllClaims:从 JWT 令牌中提取所有申明。
UserDetailsService 类定义
接下来,咱们能够创立一个自定义的 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.stereotype.Service;
@Service
public class JwtUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {UserEntity user = userRepository.findByUsername(username);
if (user == null) {throw new UsernameNotFoundException("User not found with username:" + username);
}
return new User(user.getUsername(), user.getPassword(),
new ArrayList<>());
}
}
在这个实现中,咱们应用了 UserRepository 来检索用户信息。咱们实现了 UserDetailsService 接口,并笼罩了 loadUserByUsername 办法,以便验证用户登录信息。
JwtAuthenticationFilter 定义
接下来,咱们能够创立一个 JwtAuthenticationFilter 类,用于拦挡登录申请并生成 JWT 令牌:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
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.Collections;
import java.util.Date;
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtTokenUtil jwtTokenUtil;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil) {
this.authenticationManager = authenticationManager;
this.jwtTokenUtil = jwtTokenUtil;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {LoginRequest loginRequest = new ObjectMapper().readValue(request.getInputStr eam(), LoginRequest.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword(), Collections.emptyList())
);
} catch (IOException e) {throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)throwsIOException,ServletException {UserDetails userDetails = (UserDetails) authResult.getPrincipal();
String token = jwtTokenUtil.generateToken(userDetails);
response.addHeader("Authorization", "Bearer" + token);
}
private static class LoginRequest {
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;}
}
}
在这个实现中,咱们继承了
UsernamePasswordAuthenticationFilter 类,并笼罩了 attemptAuthentication 和 successfulAuthentication 办法,以便在登录胜利时生成 JWT 令牌并将其增加到 HTTP 响应头中。
Spring Security 配置类
最初,咱们能够创立一个 Spring Security 配置类,以便配置验证和受权规定:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtUserDetailsService jwtUserDetailsService;
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void configure(HttpSecurity http) throws Exception {http.csrf().disable()
.authorizeRequests().antMatchers("/authenticate").permitAll()
.anyRequest().authenticated().and()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(newJwtAuthenticationFilter(authenticationManager(), jwtTokenUtil), UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();
}
}
在这个实现中,咱们应用 JwtUserDetailsService 来验证用户登录信息,并应用
JwtAuthenticationEntryPoint 来解决验证谬误。
咱们还配置了 JwtAuthenticationFilter 来生成 JWT 令牌,并将其增加到 HTTP 响应头中。咱们还定义了一个 PasswordEncoderbean,用于加密用户明码。
调试接口验证
当初,咱们能够向 /authenticate 端点发送 POST 申请,以验证用户登录信息并生成 JWT 令牌。例如:
bash
curl -X POST \
http://localhost:8080/authenticate \
-H 'Content-Type: application/json'\
-d '{"username":"user","password":"password"}'
如果登录信息验证胜利,将返回一个带有 JWT 令牌的 HTTP 响应头。咱们能够应用这个令牌来拜访须要受权的端点。例如:
bash
curl -X GET \
http://localhost:8080/hello \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNjI0MDM2NzA4LCJleHAiOjE2MjQwMzc1MDh9.9fZS7jPp0NzB0JyOo4y4jO4x3s3KjV7yW1nLzV7cO_c'
在这个示例中,咱们向 /hello 端点发送 GET 申请,并在 HTTP 头中增加 JWT 令牌。如果令牌无效并且用户有权拜访该端点,则返回一个胜利的 HTTP 响应。
总结
JWT 是一种简略、平安和可扩大的身份验证机制,实用于各种应用程序和场景。它能够缩小服务器的累赘,进步应用程序的安全性,并且能够轻松地扩大到其余应用程序中。
然而 JWT 也有肯定的毛病,比方他的 payload 模块并没有明确阐明肯定要加密传输,所以当你没有额定做一些安全性措施的状况下,jwt 一旦被他人截获,很容易透露用户信息。所以,如果要减少 JWT 的在理论我的项目中的安全性,平安加固措施必不可少,包含加密形式,秘钥的保留,JWT 的过期策略等等。
当然理论中的认证鉴权框架不止有 JWT,JWT 只是解决了用户上下文传输的问题。理论我的项目中常常是 JWT 联合其余认证零碎一起应用,比方 OAuth2.0。这里篇幅无限,就不开展。当前有机会再独自写一篇对于 OAuth2.0 认证架构的文章。
作者:京东物流 赵勇萍
内容起源:京东云开发者社区