往期「譯見」系列文章在帳號分享中持續連載,敬請查看~前端
在往期「譯見」系列的文章中,咱們已經創建了業務邏輯、數據訪問層和前端控制器, 可是忽略了對身份進行驗證。隨着 Spring Security 成爲實際意義上的標準, 將會在在構建 Java web 應用程序的身份驗證和受權時使用到它。在構建用戶管理微服務系列的第五部分中, 將帶您探索 Spring Security 是如何同 JWT 令牌一塊兒使用的。git
諸如 Facebook,Github,Twitter 等大型網站都在使用基於 Token 的身份驗證。相比傳統的身份驗證方法,Token 的擴展性更強,也更安全,很是適合用在 Web 應用或者移動應用上。咱們將 Token 翻譯成令牌,也就意味着,你能依靠這個令牌去經過一些關卡,來實現驗證。實施 Token 驗證的方法不少,JWT 就是相關標準方法中的一種。github
JSON Web TOKEN(JWT)是一個開放的標準 (RFC 7519), 它定義了一種簡潔且獨立的方式, 讓在各方之間的 JSON 對象安全地傳輸信息。而通過數字簽名的信息也能夠被驗證和信任。web
JWT 的應用愈來愈普遍, 而由於它是輕量級的,你也不須要有一個用來驗證令牌的認證服務器。與 OAuth 相比, 這有利有弊。若是 JWT 令牌被截獲,它能夠用來模擬用戶, 也沒法防範使用這個被截獲的令牌繼續進行身份驗證。
算法
真正的 JWT 令牌看起來像下面這樣:
spring
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJsYXN6bG9fQVRfc3ByaW5ndW5pX0RPVF9jb20iLCJuYW1lIjoiTMOhc3psw7MgQ3NvbnRvcyIsImFkbWluIjp0cnVlfQ.
XEfFHwFGK0daC80EFZBB5ki2CwrOb7clGRGlzchAD84複製代碼
JWT 令牌的第一部分是令牌的 header , 用於標識令牌的類型和對令牌進行簽名的算法。數據庫
{
"alg": "HS256", "typ": "JWT"
}複製代碼
第二部分是 JWT 令牌的 payload 或它的聲明。這二者是有區別的。Payload 能夠是任意一組數據, 它甚至能夠是明文或其餘 (嵌入 JWT)的數據。而聲明則是一組標準的字段。json
{
"sub": "laszlo_AT_springuni_DOT_com", "name": "László Csontos", "admin": true
}複製代碼
第三部分是由算法產生的、由 JWT 的 header 表示的簽名。
後端
有至關多的第三方庫可用於操做 JWT 令牌。而在本文中, 我使用了 JJWT。緩存
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>複製代碼
採用 JwtTokenService 使 JWT 令牌從身份驗證明例中建立, 並將 JWTs 解析回身份驗證明例。
public class JwtTokenServiceImpl implements JwtTokenService {
private static final String AUTHORITIES = "authorities";
static final String SECRET = "ThisIsASecret";
@Override
public String createJwtToken(Authentication authentication, int minutes) {
Claims claims = Jwts.claims()
.setId(String.valueOf(IdentityGenerator.generate()))
.setSubject(authentication.getName())
.setExpiration(new Date(currentTimeMillis() + minutes * 60 * 1000))
.setIssuedAt(new Date());
String authorities = authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.map(String::toUpperCase)
.collect(Collectors.joining(","));
claims.put(AUTHORITIES, authorities);
return Jwts.builder()
.setClaims(claims)
.signWith(HS512, SECRET)
.compact();
}
@Override
public Authentication parseJwtToken(String jwtToken) throws AuthenticationException {
try {
Claims claims = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(jwtToken)
.getBody();
return JwtAuthenticationToken.of(claims);
} catch (ExpiredJwtException | SignatureException e) {
throw new BadCredentialsException(e.getMessage(), e);
} catch (UnsupportedJwtException | MalformedJwtException e) {
throw new AuthenticationServiceException(e.getMessage(), e);
} catch (IllegalArgumentException e) {
throw new InternalAuthenticationServiceException(e.getMessage(), e);
}
}
}複製代碼
根據實際的驗證,parseClaimsJws () 會引起各類異常。在 parseJwtToken () 中, 引起的異常被轉換回 AuthenticationExceptions。雖然 JwtAuthenticationEntryPoint 能將這些異常轉換爲各類 HTTP 的響應代碼, 但它也只是重複 DefaultAuthenticationFailureHandler 來以 http 401 (未經受權) 響應。
基本上, 認證過程有兩個短語, 讓後端將服務用於單頁面 web 應用程序。
第一次登陸變完成啓動, 且在這一過程當中, 將建立一個 JWT 令牌並將其發送回客戶端。這些是經過如下請求完成的:
POST /session
{
"username": "laszlo_AT_sprimguni_DOT_com",
"password": "secret"
}複製代碼
成功登陸後, 客戶端會像往常同樣向其餘端點發送後續請求, 並在受權的 header 中提供本地緩存的 JWT 令牌。
Authorization: Bearer <JWT token>複製代碼
正如上面的步驟所講, LoginFilter 開始進行登陸過程。而Spring Security 的內置 UsernamePasswordAuthenticationFilter 被延長, 來讓這種狀況發生。這二者之間的惟一的區別是, UsernamePasswordAuthenticationFilter 使用表單參數來捕獲用戶名和密碼, 相比之下, LoginFilter 將它們視作 JSON 對象。
import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.security.web.authentication.*;
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private static final String LOGIN_REQUEST_ATTRIBUTE = "login_request";
...
@Override
public Authentication attemptAuthentication(
HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
LoginRequest loginRequest =
objectMapper.readValue(request.getInputStream(), LoginRequest.class);
request.setAttribute(LOGIN_REQUEST_ATTRIBUTE, loginRequest);
return super.attemptAuthentication(request, response);
} catch (IOException ioe) {
throw new InternalAuthenticationServiceException(ioe.getMessage(), ioe);
} finally {
request.removeAttribute(LOGIN_REQUEST_ATTRIBUTE);
}
}
@Override
protected String obtainUsername(HttpServletRequest request) {
return toLoginRequest(request).getUsername();
}
@Override
protected String obtainPassword(HttpServletRequest request) {
return toLoginRequest(request).getPassword();
}
private LoginRequest toLoginRequest(HttpServletRequest request) { return (LoginRequest)request.getAttribute(LOGIN_REQUEST_ATTRIBUTE);
}
}複製代碼
處理登錄過程的結果將在以後分派給一個 AuthenticationSuccessHandler 和 AuthenticationFailureHandler。
二者都至關簡單。DefaultAuthenticationSuccessHandler 調用 JwtTokenService 發出一個新的令牌, 而後將其發送回客戶端。
public class DefaultAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private static final int ONE_DAY_MINUTES = 24 * 60;
private final JwtTokenService jwtTokenService;
private final ObjectMapper objectMapper;
public DefaultAuthenticationSuccessHandler(
JwtTokenService jwtTokenService, ObjectMapper objectMapper) {
this.jwtTokenService = jwtTokenService;
this.objectMapper = objectMapper;
}
@Override
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
response.setContentType(APPLICATION_JSON_VALUE);
String jwtToken = jwtTokenService.createJwtToken(authentication, ONE_DAY_MINUTES);
objectMapper.writeValue(response.getWriter(), jwtToken);
}
}複製代碼
如下是它的對應, DefaultAuthenticationFailureHandler, 只是發送回一個 http 401 錯誤消息。
public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler {
private static final Logger LOGGER =
LoggerFactory.getLogger(DefaultAuthenticationFailureHandler.class);
private final ObjectMapper objectMapper;
public DefaultAuthenticationFailureHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void onAuthenticationFailure(
HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException {
LOGGER.warn(exception.getMessage());
HttpStatus httpStatus = translateAuthenticationException(exception);
response.setStatus(httpStatus.value());
response.setContentType(APPLICATION_JSON_VALUE);
writeResponse(response.getWriter(), httpStatus, exception);
}
protected HttpStatus translateAuthenticationException(AuthenticationException exception) {
return UNAUTHORIZED;
}
protected void writeResponse(
Writer writer, HttpStatus httpStatus, AuthenticationException exception) throws IOException {
RestErrorResponse restErrorResponse = RestErrorResponse.of(httpStatus, exception);
objectMapper.writeValue(writer, restErrorResponse);
}
}複製代碼
處理後續請求
在客戶端登錄後, 它將在本地緩存 JWT 令牌, 並在前面討論的後續請求中發送反回。
對於每一個請求, JwtAuthenticationFilter 經過 JwtTokenService 驗證接收到的 JWT令牌。
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final Logger LOGGER =
LoggerFactory.getLogger(JwtAuthenticationFilter.class);
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String TOKEN_PREFIX = "Bearer";
private final JwtTokenService jwtTokenService;
public JwtAuthenticationFilter(JwtTokenService jwtTokenService) {
this.jwtTokenService = jwtTokenService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
Authentication authentication = getAuthentication(request);
if (authentication == null) {
SecurityContextHolder.clearContext();
filterChain.doFilter(request, response);
return;
}
try {
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
} finally {
SecurityContextHolder.clearContext();
}
} private Authentication getAuthentication(HttpServletRequest request) {
String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER); if (StringUtils.isEmpty(authorizationHeader)) {
LOGGER.debug("Authorization header is empty.");
return null;
} if (StringUtils.substringMatch(authorizationHeader, 0, TOKEN_PREFIX)) {
LOGGER.debug("Token prefix {} in Authorization header was not found.", TOKEN_PREFIX);
return null;
}
String jwtToken = authorizationHeader.substring(TOKEN_PREFIX.length() + 1); try {
return jwtTokenService.parseJwtToken(jwtToken);
} catch (AuthenticationException e) {
LOGGER.warn(e.getMessage());
return null;
}
}
}複製代碼
若是令牌是有效的, 則會實例化 JwtAuthenticationToken, 並執行線程的 SecurityContext。而因爲恢復的 JWT 令牌包含惟一的 ID 和通過身份驗證的用戶的權限, 所以無需與數據庫聯繫以再次獲取此信息。
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private static final String AUTHORITIES = "authorities";
private final long userId;
private JwtAuthenticationToken(long userId, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.userId = userId;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Long getPrincipal() {
return userId;
} /** * Factory method for creating a new {@code {@link JwtAuthenticationToken}}. * @param claims JWT claims * @return a JwtAuthenticationToken */
public static JwtAuthenticationToken of(Claims claims) {
long userId = Long.valueOf(claims.getSubject());
Collection<GrantedAuthority> authorities =
Arrays.stream(String.valueOf(claims.get(AUTHORITIES)).split(","))
.map(String::trim)
.map(String::toUpperCase)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(userId, authorities);
Date now = new Date();
Date expiration = claims.getExpiration();
Date notBefore = claims.getNotBefore();
jwtAuthenticationToken.setAuthenticated(now.after(notBefore) && now.before(expiration)); return jwtAuthenticationToken;
}
}複製代碼
在這以後, 它由安全框架決定是否容許或拒絕請求。
Spring Security 在 Java EE 世界中有競爭者嗎?
雖然這不是這篇文章的主題, 但我想花一分鐘的時間來談談。若是我不得不在一個 JAVA EE 應用程序中完成全部這些?Spring Security 真的是在 JAVA 中實現身份驗證和受權的黃金標準嗎?
讓咱們作個小小的研究!
JAVA EE 8 指日可待,他將在 2017 年年末發佈,我想看看它是否會是 Spring Security 一個強大的競爭者。我發現 JAVA EE 8 將提供 JSR-375 , 這應該會緩解 JAVA EE 應用程序的安全措施的發展。它的參考實施被稱爲 Soteira, 是一個相對新的 github 項目。那就是說, 如今的答案是真的沒有這樣的一個競爭者。
但這項研究是不完整的,並無提到 Apache Shiro。雖然我從未使用過,但我據說這算是更爲簡單的 Spring Security。讓它更 JWT 令牌 一塊兒使用也不是不可能。從這個角度來看,Apache Shiro 是算 Spring Security 的一個的有可比性的替代品
下期預告:構建用戶管理微服務(六):添加持久 JWT 令牌的身份驗證
原文連接:https://www.springuni.com/user-management-microservice-part-5