翻译自: http://svlada.com/jwt-token-authentication-with-spring-boot/
目录
- 简介
- 阅前准备
- Ajax认证
- 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": "[email protected]",
"password": "test1234"
}
CURL:
curl -X POST -H "X-Requested-With: XMLHttpRequest" -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '{
"username": "[email protected]",
"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": "[email protected]",
"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": "[email protected]",
"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 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 tokenMap = new HashMap();
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
在这个示例中我们用到了Stormpath的 Java JWT 包
确保在pom.xml中配置了JJWT
io.jsonwebtoken
jjwt
${jjwt.version}
我们需要建一个工厂类 (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相比提供了更为重要的好处:
- 跨域(CORS)
- 不需要CSRF保护
- 更好地与移动端集成
- 减轻认证服务器压力
- 不需要分布式会话存储
用这种方式的一些取舍:
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资源的签名请求
以
在我们的实例请求头名称(
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 jwsClaims = rawAccessToken.parseClaims(jwtSettings.getTokenSigningKey());
String subject = jwsClaims.getBody().getSubject();
List scopes = jwsClaims.getBody().get("scopes", List.class);
List 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 pathsToSkip, String processingPath) {
Assert.notNull(pathsToSkip);
List 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 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?