JWT认证教程:使用Spring Boot的例子

翻译自: http://svlada.com/jwt-token-authentication-with-spring-boot/

目录

  1. 简介
  2. 阅前准备
  3. Ajax认证
  4. JWT认证

简介

这篇文章将会指导你怎么用springboot实现JWT认证

我们将介绍以下两种情况:

  • Ajax 认证
  • JWT Token 认证

阅前准备

在阅读文章之前请从GitHub:
https://github.com/svlada/springboot-security-jwt
检出示例代码
这个项目使用H2内存数据库存储简单的用户信息。为了方便我已经配置好了springboot在启动时加载数据库((/jwt-demo/src/main/resources/data.sql))

项目整体结构如下

+---main
|   +---java
|   |   \---com
|   |       \---svlada
|   |           +---common
|   |           +---entity
|   |           +---profile
|   |           |   \---endpoint
|   |           +---security
|   |           |   +---auth
|   |           |   |   +---ajax
|   |           |   |   \---jwt
|   |           |   |       +---extractor
|   |           |   |       \---verifier
|   |           |   +---config
|   |           |   +---endpoint
|   |           |   +---exceptions
|   |           |   \---model
|   |           |       \---token
|   |           \---user
|   |               +---repository
|   |               \---service
|   \---resources
|       +---static
|       \---templates

Ajax 认证

当我们谈论Ajax认证时,通常是指把用户提供的证书以json串格式当做XMLHttpRequest的一部分发送请求的过程

在本教程的第一部分中,Ajax认证是通过遵循Spring Security的标准模式实现的

下面是我们将要实现的组件列表:

1.AjaxLoginProcessingFilter
2.AjaxAuthenticationProvider
3.AjaxAwareAuthenticationSuccessHandler
4.AjaxAwareAuthenticationFailureHandler
5.RestAuthenticationEntryPoint
6.WebSecurityConfig

查看实现细节之前,先看看request/response的认证流程

Ajax认证请求例子
这个认证API允许用户发送证书去获取认证令牌(authentication token)

这个例子中,客户端通过调用API接口((/api/auth/login))发起的认证过程

HTTP请求(postman):

POST /api/auth/login HTTP/1.1  
Host: localhost:9966  
X-Requested-With: XMLHttpRequest  
Content-Type: application/json  
Cache-Control: no-cache
{
    "username": "svlada@gmail.com",
    "password": "test1234"
}

CURL:

curl -X POST -H "X-Requested-With: XMLHttpRequest" -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '{  
    "username": "svlada@gmail.com",
    "password": "test1234"
}' "http://localhost:9966/api/auth/login"

Ajax认证返回示例
如果客户端提供的凭据是有效的,请求响应后返回以下信息:

1.HTTP status "200 OK"
2.Signed JWT Access and Refresh tokens are included in the response body

JWT Access token - 用于对保护的 API 进行验证 . 必须放在 X-Authorization 的请求头中.

JWT Refresh token -用来获取新的 Access Token. 调用: /api/auth/token 来获取新token.

响应示例

{
"token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1BSRU1JVU1fTUVNQkVSIl0sImlzcyI6Imh0dHA6Ly9zdmxhZGEuY29tIiwiaWF0IjoxNDcyMDMzMzA4LCJleHAiOjE0NzIwMzQyMDh9.41rxtplFRw55ffqcw1Fhy2pnxggssdWUU8CDOherC0Kw4sgt3-rw_mPSWSgQgsR0NLndFcMPh7LSQt5mkYqROQ",
"refreshToken": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfUkVGUkVTSF9UT0tFTiJdLCJpc3MiOiJodHRwOi8vc3ZsYWRhLmNvbSIsImp0aSI6IjkwYWZlNzhjLTFkMmUtNDg2OS1hNzdlLTFkNzU0YjYwZTBjZSIsImlhdCI6MTQ3MjAzMzMwOCwiZXhwIjoxNDcyMDM2OTA4fQ.SEEG60YRznBB2O7Gn_5X6YbRmyB3ml4hnpSOxqkwQUFtqA6MZo7_n2Am2QhTJBJA1Ygv74F2IxiLv0urxGLQjg"
}

Ajax认证返回示例

JWT Access Token

JWT Access token用来身份验证和授权

1.身份验证是通过校验JWT Access Token的签名。如果签名被证明是有效的,那么就可以访问所请求的API资源。
2.授权是查看JWT Access Token权限属性范围

JWT Access token 包含三个部分:头(Header)、 主体(Claims)、签名(Signature)

Header

{
    "alg": "HS512"
}

Claims

{
  "sub": "svlada@gmail.com",
  "scopes": [
    "ROLE_ADMIN",
    "ROLE_PREMIUM_MEMBER"
  ],
  "iss": "http://svlada.com",
  "iat": 1472033308,
  "exp": 147
}

Signature (base64 encoded)

41rxtplFRw55ffqcw1Fhy2pnxggssdWUU8CDOherC0Kw4sgt3-rw_mPSWSgQgsR0NLndFcMPh7LSQt5mkYqROQ  
JWT Refresh Token

Refresh token是个长存的token用来获取新的Access tokens.过期时间比Access token长.
这个示例中我们用jti claim 去维护黑明道或者撤销令牌列表. JWT ID(jti) claim 由 RFC7519 定义用于标识单个Refresh token。

JWT Refresh token 包含三个部分:头(Header)、 主体(Claims)、签名(Signature)

Header

{
  "alg": "HS512"
}

Claims

{
  "sub": "svlada@gmail.com",
  "scopes": [
    "ROLE_REFRESH_TOKEN"
  ],
  "iss": "http://svlada.com",
  "jti": "90afe78c-1d2e-4869-a77e-1d754b60e0ce",
  "iat": 1472033308,
  "exp": 1472036908
}

Signature (base64 encoded)

SEEG60YRznBB2O7Gn_5X6YbRmyB3ml4hnpSOxqkwQUFtqA6MZo7_n2Am2QhTJBJA1Ygv74F2IxiLv0urxGLQjg
AjaxLoginProcessingFilter

第一步扩展AjaxLoginProcessingFilter处理Ajax验证请求

用attemptAuthentication 解析并验证(是否为空)得到的json串
成功验证后的json串交由AjaxAuthenticationProvider进行业务逻辑校验(数据库或其它系统里面有没有)
认证成功调用successfulauthentication
认证失败调用unsuccessfulAuthentication

public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {  
    private static Logger logger = LoggerFactory.getLogger(AjaxLoginProcessingFilter.class);

    private final AuthenticationSuccessHandler successHandler;
    private final AuthenticationFailureHandler failureHandler;

    private final ObjectMapper objectMapper;

    public AjaxLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler, 
            AuthenticationFailureHandler failureHandler, ObjectMapper mapper) {
        super(defaultProcessUrl);
        this.successHandler = successHandler;
        this.failureHandler = failureHandler;
        this.objectMapper = mapper;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {
        if (!HttpMethod.POST.name().equals(request.getMethod()) || !WebUtil.isAjax(request)) {
            if(logger.isDebugEnabled()) {
                logger.debug("Authentication method not supported. Request method: " + request.getMethod());
            }
            throw new AuthMethodNotSupportedException("Authentication method not supported");
        }

        LoginRequest loginRequest = objectMapper.readValue(request.getReader(), LoginRequest.class);

        if (StringUtils.isBlank(loginRequest.getUsername()) || StringUtils.isBlank(loginRequest.getPassword())) {
            throw new AuthenticationServiceException("Username or Password not provided");
        }

        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());

        return this.getAuthenticationManager().authenticate(token);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
            Authentication authResult) throws IOException, ServletException {
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        failureHandler.onAuthenticationFailure(request, response, failed);
    }
}
AjaxAuthenticationProvider

AjaxAuthenticationProvider 做了下面几件事:
1.根据数据库、LDAP或其他保存用户数据的系统验证用户凭据
2.如果用户名和密码不匹配数据库中的记录,则抛出异常。
3.创建用户(这个例子中只有用户名和权限)
4、认证成功后用AjaxAwareAuthenticationSuccessHandler这个方法制作令牌

@Component
public class AjaxAuthenticationProvider implements AuthenticationProvider {  
    private final BCryptPasswordEncoder encoder;
    private final DatabaseUserService userService;

    @Autowired
    public AjaxAuthenticationProvider(final DatabaseUserService userService, final BCryptPasswordEncoder encoder) {
        this.userService = userService;
        this.encoder = encoder;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.notNull(authentication, "No authentication data provided");

        String username = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();

        User user = userService.getByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));

        if (!encoder.matches(password, user.getPassword())) {
            throw new BadCredentialsException("Authentication Failed. Username or Password not valid.");
        }

        if (user.getRoles() == null) throw new InsufficientAuthenticationException("User has no roles assigned");

        List<GrantedAuthority> authorities = user.getRoles().stream()
                .map(authority -> new SimpleGrantedAuthority(authority.getRole().authority()))
                .collect(Collectors.toList());

        UserContext userContext = UserContext.create(user.getUsername(), authorities);

        return new UsernamePasswordAuthenticationToken(userContext, null, userContext.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
    }
}
AjaxAwareAuthenticationSuccessHandler

当用户验证成功,我们要实现authenticationsuccesshandler接口

用AuthenticationSuccessHandler该接口的实现类AjaxAwareAuthenticationSuccessHandler把包含JWT Access token和 Refresh token的json串放到HTTP response body中

@Component
public class AjaxAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {  
    private final ObjectMapper mapper;
    private final JwtTokenFactory tokenFactory;

    @Autowired
    public AjaxAwareAuthenticationSuccessHandler(final ObjectMapper mapper, final JwtTokenFactory tokenFactory) {
        this.mapper = mapper;
        this.tokenFactory = tokenFactory;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        UserContext userContext = (UserContext) authentication.getPrincipal();

        JwtToken accessToken = tokenFactory.createAccessJwtToken(userContext);
        JwtToken refreshToken = tokenFactory.createRefreshToken(userContext);

        Map<String, String> tokenMap = new HashMap<String, String>();
        tokenMap.put("token", accessToken.getToken());
        tokenMap.put("refreshToken", refreshToken.getToken());

        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        mapper.writeValue(response.getWriter(), tokenMap);

        clearAuthenticationAttributes(request);
    }

    /**
     * Removes temporary authentication-related data which may have been stored
     * in the session during the authentication process..
     * 
     */
    protected final void clearAuthenticationAttributes(HttpServletRequest request) {
        HttpSession session = request.getSession(false);

        if (session == null) {
            return;
        }

        session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    }
}

重点看一下如何创建JWT Access token
在这个示例中我们用到了StormpathJava JWT

确保在pom.xml中配置了JJWT

<dependency>  
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>${jjwt.version}</version>
</dependency> 

我们需要建一个工厂类 (JwtTokenFactory)去解耦token创建逻辑
JwtTokenFactory#createAccessJwtToken 方法创建JWT Access token签名
JwtTokenFactory#createRefreshToken 方法创建JWT Refresh token签名

@Component
public class JwtTokenFactory {  
    private final JwtSettings settings;

    @Autowired
    public JwtTokenFactory(JwtSettings settings) {
        this.settings = settings;
    }

    /**
     * Factory method for issuing new JWT Tokens.
     * 
     * @param username
     * @param roles
     * @return
     */
    public AccessJwtToken createAccessJwtToken(UserContext userContext) {
        if (StringUtils.isBlank(userContext.getUsername())) 
            throw new IllegalArgumentException("Cannot create JWT Token without username");

        if (userContext.getAuthorities() == null || userContext.getAuthorities().isEmpty()) 
            throw new IllegalArgumentException("User doesn't have any privileges");

        Claims claims = Jwts.claims().setSubject(userContext.getUsername());
        claims.put("scopes", userContext.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList()));

        DateTime currentTime = new DateTime();

        String token = Jwts.builder()
          .setClaims(claims)
          .setIssuer(settings.getTokenIssuer())
          .setIssuedAt(currentTime.toDate())
          .setExpiration(currentTime.plusMinutes(settings.getTokenExpirationTime()).toDate())
          .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
        .compact();

        return new AccessJwtToken(token, claims);
    }

    public JwtToken createRefreshToken(UserContext userContext) {
        if (StringUtils.isBlank(userContext.getUsername())) {
            throw new IllegalArgumentException("Cannot create JWT Token without username");
        }

        DateTime currentTime = new DateTime();

        Claims claims = Jwts.claims().setSubject(userContext.getUsername());
        claims.put("scopes", Arrays.asList(Scopes.REFRESH_TOKEN.authority()));

        String token = Jwts.builder()
          .setClaims(claims)
          .setIssuer(settings.getTokenIssuer())
          .setId(UUID.randomUUID().toString())
          .setIssuedAt(currentTime.toDate())
          .setExpiration(currentTime.plusMinutes(settings.getRefreshTokenExpTime()).toDate())
          .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
        .compact();

        return new AccessJwtToken(token, claims);
    }
}

注意!如果你要在Jwts.builder()外面实例化Claims对象,必须确保先调用Jwts.builder()#setClaims(claims)方法.
如果不这样做Jwts.builder默认情况下会创建一个空的Claims对象。这是什么意思?如果你在调用Jwts.builder()#setClaims() 之后
用Jwts.builder()#setSubject()去设值,它将会丢失
简单的来讲新的Claims类实例将覆盖Jwts.builder()创建的默认Claims

AjaxAwareAuthenticationFailureHandler

当在AjaxLoginProcessingFilter中认证失败会调用AjaxAwareAuthenticationFailureHandler。你可以根据认证过程中发生的异常自定义一些错误信息

@Component
public class AjaxAwareAuthenticationFailureHandler implements AuthenticationFailureHandler {  
    private final ObjectMapper mapper;

    @Autowired
    public AjaxAwareAuthenticationFailureHandler(ObjectMapper mapper) {
        this.mapper = mapper;
    }   

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException e) throws IOException, ServletException {

        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);

        if (e instanceof BadCredentialsException) {
            mapper.writeValue(response.getWriter(), ErrorResponse.of("Invalid username or password", ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
        } else if (e instanceof JwtExpiredTokenException) {
            mapper.writeValue(response.getWriter(), ErrorResponse.of("Token has expired", ErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED));
        } else if (e instanceof AuthMethodNotSupportedException) {
            mapper.writeValue(response.getWriter(), ErrorResponse.of(e.getMessage(), ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
        }

        mapper.writeValue(response.getWriter(), ErrorResponse.of("Authentication failed", ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
    }
}

JWT Authentication

最近基于token的身份验证模式变得很流行,因为它比session和cookie相比提供了更为重要的好处:

  1. 跨域(CORS)
  2. 不需要CSRF保护
  3. 更好地与移动端集成
  4. 减轻认证服务器压力
  5. 不需要分布式会话存储

用这种方式的一些取舍:
1.更容易受 XSS 攻击
2.Access token 可以包含过期的授权声明 (例如:当用户的权限被撤销)
3.Access tokens 在授权增加时会变的越来越大
4.可能很难实现文件下载API
5.真正的无状态和撤销是互斥的

在这篇文章中我们将研究如何用JWT进行基于token的认证

JWT认证流程非常简单:
1.用户通过向授权服务器提供凭据(用户登录)获取Refresh 和 Access token
2.用户访问每个受保护的api资源时都要发送Access token
3.Access token包含用户标识(例如用户ID)和授权声明。

需要注意的是,授权声明将包含在Access token中。为什么那么重要呢?
假设授权声明(例如数据库中的用户权限)在Access token的生命周期期间发生了更改,在新的Access token生效
之前这些改变不会生效。
大多数情况下,这都不是什么问题应为,Access token是很短暂的,否则就使用opaque token模式(每次都创建新的)

在了解实现细节之前,让我们看看请求受保护API资源的示例。

对受保护的api资源的签名请求

以<header-name> Bearer <access_token>格式发送access token
在我们的实例请求头名称(<header-name>) 用X-Authorization

GET /api/me HTTP/1.1  
Host: localhost:9966  
X-Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1BSRU1JVU1fTUVNQkVSIl0sImlzcyI6Imh0dHA6Ly9zdmxhZGEuY29tIiwiaWF0IjoxNDcyMzkwMDY1LCJleHAiOjE0NzIzOTA5NjV9.Y9BR7q3f1npsSEYubz-u8tQ8dDOdBcVPFN7AIfWwO37KyhRugVzEbWVPO1obQlHNJWA0Nx1KrEqHqMEjuNWo5w  
Cache-Control: no-cache  

CURL

curl -X GET 
-H "X-Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1BSRU1JVU1fTUVNQkVSIl0sImlzcyI6Imh0dHA6Ly9zdmxhZGEuY29tIiwiaWF0IjoxNDcyMzkwMDY1LCJleHAiOjE0NzIzOTA5NjV9.Y9BR7q3f1npsSEYubz-u8tQ8dDOdBcVPFN7AIfWwO37KyhRugVzEbWVPO1obQlHNJWA0Nx1KrEqHqMEjuNWo5w" 
-H "Cache-Control: no-cache" "http://localhost:9966/api/me"  

实现细节,以下是我们要实现的JWT认证组件:

1. JwtTokenAuthenticationProcessingFilter
2. JwtAuthenticationProvider
3. SkipPathRequestMatcher
4. JwtHeaderTokenExtractor
5. BloomFilterTokenVerifier
6. WebSecurityConfig
JwtTokenAuthenticationProcessingFilter

JwtTokenAuthenticationProcessingFilter 作用与除了刷新token(/api/auth/token)和登录 (/api/auth/login)之外的每一个以api开头 (/api/**)的请求
这个过滤器做了以下下这些事:
1.检查从请求头X-Authorization中获取的access token,如果发现了Access token,则表示委托JwtAuthenticationProvider进行验证否则抛认证异常
2.根据JwtAuthenticationProvider的验证结果调用成功或失败的处理策略

确保在认证成功的时候调用chain.doFilter(request, response)

FilterSecurityInterceptor#doFilter实际上在调用你的控制器中的方法去请求api资源

public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {  
    private final AuthenticationFailureHandler failureHandler;
    private final TokenExtractor tokenExtractor;

    @Autowired
    public JwtTokenAuthenticationProcessingFilter(AuthenticationFailureHandler failureHandler, 
            TokenExtractor tokenExtractor, RequestMatcher matcher) {
        super(matcher);
        this.failureHandler = failureHandler;
        this.tokenExtractor = tokenExtractor;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {
        String tokenPayload = request.getHeader(WebSecurityConfig.JWT_TOKEN_HEADER_PARAM);
        RawAccessJwtToken token = new RawAccessJwtToken(tokenExtractor.extract(tokenPayload));
        return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token));
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
            Authentication authResult) throws IOException, ServletException {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authResult);
        SecurityContextHolder.setContext(context);
        chain.doFilter(request, response);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        failureHandler.onAuthenticationFailure(request, response, failed);
    }
}
JwtHeaderTokenExtractor

JwtHeaderTokenExtractor 是个非常简单的类用于从header中提取Authorization token。
你可以实现TokenExtractor接口并且提供自定义从请求URL中提取token

@Component
public class JwtHeaderTokenExtractor implements TokenExtractor {  
    public static String HEADER_PREFIX = "Bearer ";

    @Override
    public String extract(String header) {
        if (StringUtils.isBlank(header)) {
            throw new AuthenticationServiceException("Authorization header cannot be blank!");
        }

        if (header.length() < HEADER_PREFIX.length()) {
            throw new AuthenticationServiceException("Invalid authorization header size.");
        }

        return header.substring(HEADER_PREFIX.length(), header.length());
    }
}
JwtAuthenticationProvider

JwtAuthenticationProvider做了以下下这些事:
1.验证access token签名
2.从access token中验证身份和授权并用它们创建用户
3.如果access token格式错误、过期或简单如果token没有签署合适的密钥,将抛出AuthenticationException

@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {  
    private final JwtSettings jwtSettings;

    @Autowired
    public JwtAuthenticationProvider(JwtSettings jwtSettings) {
        this.jwtSettings = jwtSettings;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();

        Jws<Claims> jwsClaims = rawAccessToken.parseClaims(jwtSettings.getTokenSigningKey());
        String subject = jwsClaims.getBody().getSubject();
        List<String> scopes = jwsClaims.getBody().get("scopes", List.class);
        List<GrantedAuthority> authorities = scopes.stream()
                .map(authority -> new SimpleGrantedAuthority(authority))
                .collect(Collectors.toList());

        UserContext context = UserContext.create(subject, authorities);

        return new JwtAuthenticationToken(context, context.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
    }
}
SkipPathRequestMatcher

JwtTokenAuthenticationProcessingFilter 配置跳过这个两个地址: /api/auth/login 和 /api/auth/token
这里用SkipPathRequestMatcher 实现RequestMatcher接口

public class SkipPathRequestMatcher implements RequestMatcher {  
    private OrRequestMatcher matchers;
    private RequestMatcher processingMatcher;

    public SkipPathRequestMatcher(List<String> pathsToSkip, String processingPath) {
        Assert.notNull(pathsToSkip);
        List<RequestMatcher> m = pathsToSkip.stream().map(path -> new AntPathRequestMatcher(path)).collect(Collectors.toList());
        matchers = new OrRequestMatcher(m);
        processingMatcher = new AntPathRequestMatcher(processingPath);
    }

    @Override
    public boolean matches(HttpServletRequest request) {
        if (matchers.matches(request)) {
            return false;
        }
        return processingMatcher.matches(request) ? true : false;
    }
}
WebSecurityConfig

WebSecurityConfig 继承 WebSecurityConfigurerAdapter 踢狗自定义安全配置。
配置和实例化了下面这些类:
1.AjaxLoginProcessingFilter
2.JwtTokenAuthenticationProcessingFilter
3.AuthenticationManager
4.BCryptPasswordEncoder

此外,在 WebSecurityConfig#configure(HttpSecurity http) 方法中,我们要配置规定的 protected/unprotected 地址
请注意,因为我们没有用cookie所以要禁用CSRF

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {  
    public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
    public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
    public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
    public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";

    @Autowired private RestAuthenticationEntryPoint authenticationEntryPoint;
    @Autowired private AuthenticationSuccessHandler successHandler;
    @Autowired private AuthenticationFailureHandler failureHandler;
    @Autowired private AjaxAuthenticationProvider ajaxAuthenticationProvider;
    @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider;

    @Autowired private TokenExtractor tokenExtractor;

    @Autowired private AuthenticationManager authenticationManager;

    @Autowired private ObjectMapper objectMapper;

    protected AjaxLoginProcessingFilter buildAjaxLoginProcessingFilter() throws Exception {
        AjaxLoginProcessingFilter filter = new AjaxLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper);
        filter.setAuthenticationManager(this.authenticationManager);
        return filter;
    }

    protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
        List<String> pathsToSkip = Arrays.asList(TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT);
        SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
        JwtTokenAuthenticationProcessingFilter filter 
            = new JwtTokenAuthenticationProcessingFilter(failureHandler, tokenExtractor, matcher);
        filter.setAuthenticationManager(this.authenticationManager);
        return filter;
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(ajaxAuthenticationProvider);
        auth.authenticationProvider(jwtAuthenticationProvider);
    }

    @Bean
    protected BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .csrf().disable() // We don't need CSRF for JWT based authentication
        .exceptionHandling()
        .authenticationEntryPoint(this.authenticationEntryPoint)

        .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

        .and()
            .authorizeRequests()
                .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
                .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
                .antMatchers("/console").permitAll() // H2 Console Dash-board - only for testing
        .and()
            .authorizeRequests()
                .antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected API End-points
        .and()
            .addFilterBefore(buildAjaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
            .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}
PasswordEncoderConfig

AjaxAuthenticationProvider提供BCrypt编码

@Configuration
public class PasswordEncoderConfig {  
    @Bean
    protected BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
BloomFilterTokenVerifier

这是个虚拟类。你需要实现自己的TokenVerifier去检查撤销token

@Component
public class BloomFilterTokenVerifier implements TokenVerifier {  
    @Override
    public boolean verify(String jti) {
        return true;
    }
}

结语

我听到人们在网上窃窃私语丢失了JWT token就像丢了你的房间钥匙。所以务必小心

参考

I don’t see the point in Revoking or Blacklisting JWT

Spring Security Architecture - Dave Syer

Invalidating JWT

Secure and stateless JWT implementation

Learn JWT

Opaque access tokens and cloud foundry

The unspoken vulnerability of JWTS

How To Control User Identity Within Micro-services

Why Does OAuth v2 Have Both Access and Refresh Tokens?

RFC-6749

Are breaches of JWT-based servers more damaging?

推荐阅读更多精彩内容