一俐筋、安全
安全是方便的敵人儿礼,反之亦然。對(duì)于虛擬或真實(shí)的系統(tǒng),從物理入口到網(wǎng)絡(luò)銀行平臺(tái)榕堰,這一說法是真實(shí)的氓润。工程師不斷嘗試為給定的用例找到適當(dāng)?shù)钠胶忤档郏瑑A斜到一邊或另一側(cè)叔锐。
通常,當(dāng)出現(xiàn)新的威脅時(shí)紧帕,我們轉(zhuǎn)向安全盔然,遠(yuǎn)離方便。然后是嗜,我們看看是否可以恢復(fù)一些丟失的方便愈案,而不會(huì)降低安全性。此外鹅搪,這個(gè)惡性循環(huán)也永遠(yuǎn)存在站绪。
安全是方便的敵人,反之亦然涩嚣。
讓我們來看看REST服務(wù)目前在安全性和便利性方面的地位崇众。REST(代表代理狀態(tài)轉(zhuǎn)移)服務(wù)作為極其簡(jiǎn)化的Web服務(wù)開始掂僵,具有巨大的規(guī)范和繁瑣的格式航厚,例如:用于描述服務(wù)的 <u>WSDL</u> 或用于指定消息格式的 <u>SOAP</u>顷歌。在 REST 中,我們沒有一個(gè)幔睬。我們可以在純文本文件中描述 REST 服務(wù)眯漩,并使用我們想要的任何消息格式,如 JSON麻顶,XML赦抖,甚至是純文本。簡(jiǎn)化的方法也適用于 REST 服務(wù)的安全性; 沒有定義的標(biāo)準(zhǔn)強(qiáng)加了一種特定的方式來驗(yàn)證用戶辅肾。
雖然REST服務(wù)沒有太多的規(guī)定队萤,但重要的是缺少狀態(tài)。這意味著服務(wù)器不保留任何客戶端狀態(tài)矫钓,會(huì)話是一個(gè)很好的例子要尔。因此,服務(wù)器回復(fù)每個(gè)請(qǐng)求新娜,就好像它是客戶端的第一個(gè)赵辕。然而,即使現(xiàn)在概龄,許多實(shí)現(xiàn)仍然使用基于 cookie 的身份驗(yàn)證还惠,這是從標(biāo)準(zhǔn)網(wǎng)站架構(gòu)設(shè)計(jì)繼承的。REST 的無(wú)狀態(tài)方法使得會(huì)話 cookie 從安全的角度來看是不合適的私杜,但是它們?nèi)匀槐粡V泛使用蚕键。除了忽視所需的無(wú)國(guó)籍之外,簡(jiǎn)化的做法也是預(yù)期的安全性權(quán)衡衰粹。與用于 Web 服務(wù)的 WS-Security 標(biāo)準(zhǔn)相比锣光,創(chuàng)建和使用 REST 服務(wù)要容易得多,因此方便通過了屋頂寄猩。權(quán)衡是非常苗條的安全;
在嘗試從服務(wù)器中刪除客戶端會(huì)話時(shí)嫉晶,有些其他方法已經(jīng)被偶爾使用,比如 Basic 或 Digest HTTP 認(rèn)證田篇。兩者都使用一個(gè) Authorization
標(biāo)頭來傳送用戶憑證替废,并添加一些編碼(HTTP Basic)或加密(HTTP Digest)。當(dāng)然泊柬,它們?cè)诰W(wǎng)站上也出現(xiàn)了同樣的缺陷:HTTP Basic 必須通過 HTTPS 使用椎镣,因?yàn)橛脩裘兔艽a以易于逆轉(zhuǎn)的 base64 編碼發(fā)送,而 HTTP 摘要強(qiáng)制使用被證明是不安全的過時(shí)的 MD5 哈希值兽赁。
最后状答,一些實(shí)現(xiàn)使用任意令牌來驗(yàn)證客戶端冷守。這個(gè)選項(xiàng)似乎是我們現(xiàn)在最好的。如果正確實(shí)現(xiàn)惊科,它會(huì)修復(fù) HTTP Basic拍摇,HTTP Digest 或會(huì)話 cookie 的所有安全問題,使用起來很簡(jiǎn)單馆截,并且遵循無(wú)狀態(tài)模式充活。
然而,使用這種任意令牌蜡娶,所涉及的標(biāo)準(zhǔn)很少混卵。每個(gè)服務(wù)提供商都有他或她的想法放在令牌中,以及如何編碼或加密它窖张。來自不同提供商的消費(fèi)服務(wù)需要額外的設(shè)置時(shí)間幕随,只是為了適應(yīng)所使用的特定令牌格式。另一方面宿接,其他方法(會(huì)話 cookie赘淮,HTTP Basic 和 HTTP 摘要)是開發(fā)人員所熟知的,幾乎所有設(shè)備上的所有瀏覽器都可以開箱即用澄阳∮抵框架和語(yǔ)言已經(jīng)準(zhǔn)備好了這些方法,內(nèi)置函數(shù)可以無(wú)縫地處理碎赢。
二低剔、JWT
JWT(從 JSON Web Token 縮寫)是通常使用令牌進(jìn)行身份驗(yàn)證的缺少的標(biāo)準(zhǔn)化,不僅適用于 REST 服務(wù)肮塞。目前襟齿,草案狀態(tài)為 <u>RFC 7519</u>。它是強(qiáng)大的枕赵,可以攜帶大量的信息猜欺,但即使它的尺寸相對(duì)較小,仍然使用起來很簡(jiǎn)單拷窜。像任何其他令牌一樣开皿,JWT 可以用于在身份提供商和服務(wù)提供商(不一定是相同的系統(tǒng))之間傳遞身份驗(yàn)證的用戶身份。它還可以承載用戶的所有權(quán)利篮昧,例如授權(quán)數(shù)據(jù)赋荆,因此服務(wù)提供商不需要進(jìn)入數(shù)據(jù)庫(kù)或外部系統(tǒng)來驗(yàn)證每個(gè)請(qǐng)求的用戶角色和權(quán)限; 從令牌中提取數(shù)據(jù)。
以下是JWT的工作原理:
客戶端通過將其憑據(jù)發(fā)送給身份提供者來登錄懊昨。
身份提供者驗(yàn)證憑據(jù); 如果一切正常窄潭,它將檢索用戶數(shù)據(jù),生成包含用于訪問服務(wù)的用戶詳細(xì)信息和權(quán)限的 JWT酵颁,并且還會(huì)在 JWT 上設(shè)置到期(可能是無(wú)限制的)嫉你。
身份提供商簽名月帝,如果需要,加密 JWT幽污,并將其發(fā)送給客戶端嚷辅,作為對(duì)具有憑據(jù)的初始請(qǐng)求的響應(yīng)。
客戶端根據(jù)身份提供商設(shè)置的到期時(shí)間限制或無(wú)限制地存儲(chǔ) JWT油挥。
客戶端將所存儲(chǔ)的 JWT 發(fā)送到服務(wù)提供商的每個(gè)請(qǐng)求的授權(quán)頭中潦蝇。
對(duì)于每個(gè)請(qǐng)求款熬,服務(wù)提供者從
Authorization
頭部接收 JWT 深寥,如果需要,對(duì)其進(jìn)行解密贤牛,驗(yàn)證簽名惋鹅,如果一切正常,則提取用戶數(shù)據(jù)和權(quán)限殉簸。僅基于這些數(shù)據(jù)闰集,并且再次查找數(shù)據(jù)庫(kù)中的進(jìn)一步細(xì)節(jié)或聯(lián)系身份提供者時(shí),它可以接受或拒絕客戶端請(qǐng)求般卑。唯一的要求是身份和服務(wù)提供商就加密達(dá)成協(xié)議武鲁,以便服務(wù)可以驗(yàn)證簽名,甚至解密哪個(gè)身份被加密蝠检。
這個(gè)流程允許很大的靈活性沐鼠,同時(shí)保持安全和容易開發(fā)。通過使用這種方法叹谁,可以輕松地向服務(wù)提供商集群添加新的服務(wù)器節(jié)點(diǎn)饲梭,只需要通過向其提供一個(gè)共享的秘密密鑰來初始化它們,只能驗(yàn)證簽名并解密令牌焰檩。不需要會(huì)話復(fù)制憔涉,數(shù)據(jù)庫(kù)同步或節(jié)點(diǎn)間通信。REST 在其充分的榮耀析苫。
JWT 和其他任意令牌之間的主要區(qū)別是令牌內(nèi)容的標(biāo)準(zhǔn)化兜叨。另一個(gè)推薦的方法是 Authorization
使用承載方案將 JWT 令牌發(fā)送到頭部。標(biāo)題的內(nèi)容應(yīng)如下所示:
Authorization: Bearer <token>
三衩侥、實(shí)施
對(duì)于 REST 服務(wù)国旷,如預(yù)期的那樣工作,與傳統(tǒng)的多頁(yè)面網(wǎng)站相比顿乒,我們需要稍微不同的授權(quán)方法议街。
當(dāng)客戶端請(qǐng)求安全資源時(shí),REST 服務(wù)器不會(huì)通過重定向到登錄頁(yè)面來觸發(fā)身份驗(yàn)證過程璧榄,因此 REST 服務(wù)器使用請(qǐng)求本身可用的數(shù)據(jù)(在這種情況下為 JWT 令牌)來認(rèn)證所有請(qǐng)求特漩。如果這樣的認(rèn)證失敗吧雹,重定向就沒有意義了。REST API 只是發(fā)送 HTTP 代碼 401(未經(jīng)授權(quán))的響應(yīng)涂身,客戶端應(yīng)該知道該怎么做; 例如雄卷,瀏覽器將顯示動(dòng)態(tài) div,以允許用戶提供用戶名和密碼蛤售。
另一方面丁鹉,在經(jīng)典的多頁(yè)面網(wǎng)站中成功的認(rèn)證之后,用戶通過使用HTTP代碼 301(永久移動(dòng))來重定向悴能,通常到主頁(yè)揣钦,或者甚至更好地到達(dá)用戶最初請(qǐng)求觸發(fā)的頁(yè)面認(rèn)證過程。使用 REST漠酿,這再?zèng)]有任何意義冯凹。相反,我們將繼續(xù)執(zhí)行請(qǐng)求炒嘲,就像資源根本不安全一樣宇姚,返回 HTTP 代碼 200(OK)和預(yù)期的響應(yīng)體。
四夫凸、Spring Security
現(xiàn)在,我們來看看如何使用 <u>Java</u> 和 <u>Spring</u> 來 <u>實(shí)現(xiàn)</u> 基于JWT 令牌的 REST API夭拌,同時(shí)嘗試重用 Spring 安全性默認(rèn)行為魔熏。正如預(yù)期的那樣,Spring Security 框架帶有許多準(zhǔn)備插入的類啼止,它們處理“舊”授權(quán)機(jī)制:會(huì)話 cookie道逗,HTTP Basic 和 HTTP 摘要。然而献烦,它缺乏對(duì) JWT 的本地支持滓窍,我們需要自己動(dòng)手,使其工作巩那。
首先吏夯,我們從普通的Spring Security過濾器定義開始 web.xml
:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
請(qǐng)注意,過濾器的名稱必須恰好 springSecurityFilterChain
適用于Spring配置的其余部分才能開箱即用即横。
接下來是與安全性相關(guān)的 Spring bean 的 XML 聲明噪生。為了簡(jiǎn)化 XML,我們將 security
通過添加 xmlns="http://www.springframework.org/schema/security"
到根 XML 元素來設(shè)置默認(rèn)命名空間东囚。
其余的XML如下所示:
<global-method-security pre-post-annotations="enabled" /> (1)
<http pattern="/api/login" security="none"/> (2)
<http pattern="/api/signup" security="none"/>
<http pattern="/api/**" entry-point-ref="restAuthenticationEntryPoint" create-session="stateless"> (3)
<csrf disabled="true"/> (4)
<custom-filter before="FORM_LOGIN_FILTER" ref="jwtAuthenticationFilter"/> (5)
</http>
<beans:bean id="jwtAuthenticationFilter" class="com.toptal.travelplanner.security.JwtAuthenticationFilter"> (6)
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="authenticationSuccessHandler" ref="jwtAuthenticationSuccessHandler" /> (7)
</beans:bean>
<authentication-manager alias="authenticationManager">
<authentication-provider ref="jwtAuthenticationProvider" /> (8)
</authentication-manager>
(1)在這一行中跺嗽,我們激活
@PreFilter
,@PreAuthorize
,@PostFilter
桨嫁,@PostAuthorize
在上下文任何彈簧豆注釋植兰。(2)我們定義登錄和注冊(cè)端點(diǎn)來跳過安全性; 甚至“匿名”都應(yīng)該能夠做這兩個(gè)操作。
(3)接下來璃吧,我們定義應(yīng)用于所有請(qǐng)求的過濾器鏈楣导,同時(shí)添加兩個(gè)重要的配置:入口點(diǎn)引用和設(shè)置會(huì)話創(chuàng)建
stateless
(我們不希望為安全起見創(chuàng)建會(huì)話,因?yàn)槲覀冋跒槊總€(gè)請(qǐng)求使用令牌)畜挨。(4)我們不需要
csrf
保護(hù)筒繁,因?yàn)槲覀兊牧钆剖敲庖叩摹?/p>(5)接下來,我們?cè)?Spring 的預(yù)定義過濾器鏈中插入特殊的認(rèn)證過濾器巴元,就在 Form 登錄過濾器之前毡咏。
(6)這個(gè) bean 是我們認(rèn)證過濾器的聲明; 因?yàn)樗菙U(kuò)展Spring的
AbstractAuthenticationProcessingFilter
,我們需要用XML聲明它的屬性(自動(dòng)電線在這里不工作)务冕。我們稍后會(huì)介紹過濾器的功能血当。(7)默認(rèn)的成功處理程序
AbstractAuthenticationProcessingFilter
不足以用于 REST,因?yàn)樗鼘⒂脩糁囟ㄏ虻匠晒?yè)面; 這就是為什么我們自己設(shè)在這里禀忆。(8)
authenticationManager
由我們的過濾器使用的提供者創(chuàng)建的聲明對(duì)用戶進(jìn)行身份驗(yàn)證。
現(xiàn)在來看看我們?nèi)绾螌?shí)現(xiàn)在上面的 XML 中聲明的特定類落恼。請(qǐng)注意箩退,Spring 將為我們接線。我們從最簡(jiǎn)單的開始佳谦。
RestAuthenticationEntryPoint.java
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
// This is invoked when user tries to access a secured REST resource without supplying any credentials
// We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
如上所述戴涝,當(dāng)驗(yàn)證失敗時(shí),該類只返回 HTTP 代碼 401(未授權(quán))钻蔑,覆蓋默認(rèn)的 Spring 的重定向啥刻。
JwtAuthenticationSuccessHandler.java
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
// We do not need to do anything extra on REST authentication success, because there is no page to redirect to
}
}
此簡(jiǎn)單的覆蓋將刪除成功身份驗(yàn)證的默認(rèn)行為(重定向到家庭或用戶請(qǐng)求的任何其他頁(yè)面)。如果您想知道為什么我們不需要覆蓋它 AuthenticationFailureHandler
咪笑,那是因?yàn)槿绻丛O(shè)置重定向網(wǎng)址可帽,則默認(rèn)實(shí)現(xiàn)不會(huì)重定向到任何位置,所以我們只是避免設(shè)置 URL窗怒。
JwtAuthenticationFilter.java
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public JwtAuthenticationFilter() {
super("/**");
}
@Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return true;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
throw new JwtTokenMissingException("No JWT token found in request headers");
}
String authToken = header.substring(7);
JwtAuthenticationToken authRequest = new JwtAuthenticationToken(authToken);
return getAuthenticationManager().authenticate(authRequest);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
// As this authentication is in HTTP header, after success we need to continue the request normally
// and return the response as if the resource was not secured at all
chain.doFilter(request, response);
}
}
這個(gè)類是 JWT 身份驗(yàn)證過程的切入點(diǎn)映跟。該過濾器從請(qǐng)求頭提取JWT令牌,并將認(rèn)證委托給注入 AuthenticationManager
扬虚。如果未找到令牌努隙,將拋出異常,停止處理請(qǐng)求辜昵。我們還需要覆蓋成功的認(rèn)證荸镊,因?yàn)槟J(rèn)的 Spring 流將停止過濾器鏈并繼續(xù)執(zhí)行重定向。請(qǐng)記住,我們需要鏈條完全執(zhí)行躬存,包括生成響應(yīng)收厨,如上所述。
JwtAuthenticationProvider.java
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean supports(Class<?> authentication) {
return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
}
@Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
String token = jwtAuthenticationToken.getToken();
User parsedUser = jwtUtil.parseToken(token);
if (parsedUser == null) {
throw new JwtTokenMalformedException("JWT token is not valid");
}
List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(parsedUser.getRole());
return new AuthenticatedUser(parsedUser.getId(), parsedUser.getUsername(), token, authorityList);
}
}
在這個(gè)類中优构,我們使用 Spring 的默認(rèn)值 AuthenticationManager
诵叁,但是我們注入自己 AuthenticationProvider
的實(shí)際身份驗(yàn)證過程。為了實(shí)現(xiàn)這一點(diǎn)钦椭,我們擴(kuò)展了它 AbstractUserDetailsAuthenticationProvider
拧额,它要求我們僅 UserDetails
基于身份驗(yàn)證請(qǐng)求返回,在我們的例子中彪腔,包含在 JwtAuthenticationToken
類中的 JWT 令牌侥锦。如果令牌無(wú)效,我們拋出異常德挣。然而恭垦,如果它是有效的并且解密 JwtUtil
成功,我們將提取用戶的詳細(xì)信息(我們將在 JwtUtil
課堂上看到如何)格嗅,而不需要訪問數(shù)據(jù)庫(kù)番挺。關(guān)于用戶的所有信息(包括他或她的角色)都包含在令牌本身中。
JwtUtil.java
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
/**
* Tries to parse specified String as a JWT token. If successful, returns User object with username, id and role prefilled (extracted from token).
* If unsuccessful (token is invalid or not containing all required user properties), simply returns null.
*
* @param token the JWT token to parse
* @return the User object extracted from specified token or null if a token is invalid.
*/
public User parseToken(String token) {
try {
Claims body = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
User u = new User();
u.setUsername(body.getSubject());
u.setId(Long.parseLong((String) body.get("userId")));
u.setRole((String) body.get("role"));
return u;
} catch (JwtException | ClassCastException e) {
return null;
}
}
/**
* Generates a JWT token containing username as subject, and userId and role as additional claims. These properties are taken from the specified
* User object. Tokens validity is infinite.
*
* @param u the user for which the token will be generated
* @return the JWT token
*/
public String generateToken(User u) {
Claims claims = Jwts.claims().setSubject(u.getUsername());
claims.put("userId", u.getId() + "");
claims.put("role", u.getRole());
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
}
最后屯掖, JwtUtil
類負(fù)責(zé)將令牌解析為 User
對(duì)象玄柏,并從 User
對(duì)象生成令牌。它是直接的贴铜,因?yàn)樗褂?<u>jjwt
圖書館</u> 來完成所有的 JWT 工作粪摘。在我們的例子中,我們簡(jiǎn)單地將用戶名绍坝,用戶ID和用戶角色存儲(chǔ)在令牌中徘意。我們還可以存儲(chǔ)更多的任意東西,并添加更多的安全功能轩褐,例如令牌的到期椎咧。 AuthenticationProvider
如上所示,使用令牌的解析灾挨。該 generateToken()
方法從登錄和注冊(cè) REST 服務(wù)調(diào)用邑退,它們是不安全的,并且不會(huì)觸發(fā)任何安全檢查或要求令牌存在于請(qǐng)求中劳澄。最后地技,它會(huì)根據(jù)用戶生成將返回給客戶端的令牌。
五秒拔、結(jié)論
雖然舊的標(biāo)準(zhǔn)化安全方法(會(huì)話 cookie莫矗,HTTP Basic 和 HTTP 摘要)也可以與 REST 服務(wù)一起使用,但是它們都有一些問題,通過使用更好的標(biāo)準(zhǔn)來避免這種情況作谚。JWT 即將到來三娩,以節(jié)省時(shí)間,最重要的是非常接近成為 IETF 標(biāo)準(zhǔn)妹懒。
JWT 的主要優(yōu)勢(shì)是在無(wú)狀態(tài)的情況下處理用戶身份驗(yàn)證雀监,因此可擴(kuò)展的方式,同時(shí)保持一切安全與最新的密碼學(xué)標(biāo)準(zhǔn)眨唬。將令牌(用戶角色和權(quán)限)存儲(chǔ)在令牌本身中会前,在發(fā)布請(qǐng)求的服務(wù)器無(wú)法訪問認(rèn)證數(shù)據(jù)源的分布式系統(tǒng)體系結(jié)構(gòu)中創(chuàng)造了巨大的收益。
BY DEJAN MILOSEVIC
原文:https://www.toptal.com/java/rest-security-with-jwt-spring-security-and-java
谷歌翻譯