翻譯自: 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": "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
在這個示例中我們用到了Stormpath的 Java 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相比提供了更為重要的好處:
- 跨域(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資源的簽名請求
以<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
Secure and stateless JWT implementation
Opaque access tokens and cloud foundry
The unspoken vulnerability of JWTS
How To Control User Identity Within Micro-services