乐趣区

关于jwt:实践篇教你玩转JWT认证从一个优惠券聊起-京东云技术团队

引言

最近面试过程中,无心中跟候选人聊到了 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 长处如果咱们零碎的总结一下,如下:

  1. 跨语言和平台:JWT 是基于 JSON 规范的,因而能够在不同的编程语言和平台之间进行替换和应用。无状态:因为 JWT 蕴含所有必要的信息,服务器不须要在每个申请中存储任何会话数据,因而能够轻松地进行负载平衡。
  2. 安全性:JWT 应用数字签名来验证数据的完整性和真实性,因而能够避免数据被篡改或伪造。
  3. 可扩展性:JWT 能够蕴含任何用户信息,因而能够轻松地扩大到其余应用程序中。
  4. 一个基于 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 认证架构的文章。

作者:京东物流 赵勇萍

内容起源:京东云开发者社区

退出移动版