訪問權(quán)限校驗是一個互聯(lián)網(wǎng)系統(tǒng)必備的功能莽龟,今天我們就動手將訪問權(quán)限控制的功能加到框架中陕凹,可以針對特定路徑的接口進(jìn)行訪問權(quán)限校驗,防止出現(xiàn)越權(quán)訪問的情況發(fā)生肯适。
以前我們做權(quán)限控制都是通過用戶登陸后由服務(wù)端生成sessionid次泽,并將這個sessionid與用戶信息關(guān)聯(lián)起來穿仪,客戶端需要保存sessionid在cookie中,在后續(xù)請求交易中上送這個cookie意荤,服務(wù)端從header中獲取到cookie啊片,并從cookie中獲取到sessionid,來判斷該sessionid是否還有效玖像。在分布式紫谷、前后端分離還沒那么火的時候,權(quán)限控制基本都是基于session來實現(xiàn)的,這么做最大的缺點是笤昨,客戶端需要每次都訪問到固定的服務(wù)器節(jié)點才能查詢到sessionid祖驱,不然會導(dǎo)致查詢不到重新登陸的問題,這就需要在負(fù)載均衡F5或者nginx上配置根據(jù)sessionid做hash路由分發(fā)瞒窒,一旦某臺機(jī)器出問題需要通過這臺服務(wù)節(jié)點登陸的所有用戶全部重新登陸捺僻,影響還是很大的。隨著分布式架構(gòu)的快速發(fā)展崇裁,移動互聯(lián)的快速發(fā)展也推動了前后端分離的快速發(fā)展匕坯,最近幾年新建系統(tǒng)基本都是前后端分離分布式架構(gòu)了,這樣的架構(gòu)自然不再適合用session這種機(jī)制了拔稳,oauth2葛峻、分布式token等等授權(quán)概念紛紛推出。一般有一定規(guī)模用戶的公司會使用oauth這樣的認(rèn)證授權(quán)的架構(gòu)巴比,對于不了解oauth協(xié)議的同學(xué)建議先看看阮一峰的這篇文章术奖,寫的非常淺顯易懂,原先筆者在幾家大公司用的也是oauth匿辩,但是用過oauth的同學(xué)應(yīng)該也都知道,oauth雖然能夠在開放的前提下保證安全榛丢,但是還是有點重铲球,除非是對于要建設(shè)開放平臺的系統(tǒng)一定要用oauth,對于一般的互聯(lián)網(wǎng)項目晰赞,其實是沒必要使用oauth的稼病,oauth同樣是一種數(shù)據(jù)集中授權(quán)的模式,那么勢必還是要想辦法保證oauth token集中校驗的性能和高可用掖鱼,不管是從開發(fā)還是從硬件投入上都是一筆不小的開支然走。
所以既然要做一款輕量級的快速開發(fā)框架,那么這里我們使用了更加輕量級的token發(fā)放和驗證方案-jwt https://jwt.io戏挡,其實簡單理解就是jwt按照一定的規(guī)則生成了一個散列值作為token芍瑞,類似下圖左邊是jwt編碼后的token,右邊是原始明文組成要素主要有3部分褐墅,第一部分定義散列算法和生成類型拆檬,第二部分payload是用戶可以自定義一些用戶標(biāo)示要素項,內(nèi)部其實是使用對稱秘鑰進(jìn)行了加密這部分?jǐn)?shù)據(jù)妥凳,第三部分是校驗域用來給服務(wù)端校驗有效性用的:
并且可以通過該token還原出用戶的標(biāo)示信息竟贯,同時提供了token校驗的功能,并且不需要集中驗證逝钥,任何一臺服務(wù)節(jié)點都可以根據(jù)規(guī)則進(jìn)行校驗屑那,只要保證每臺機(jī)器生成token的秘鑰保持一致就可以。jwt是非常輕量級的一種方案,對于開發(fā)一般的項目作為權(quán)限校驗足夠了持际。
下面我們再來看看客戶端和服務(wù)端的交互流程:
client->server: 登陸
server-->client: jwt生成并返回token
client->server: 攜帶token請求接口
server->server: 通過jwt校驗token有效性
server-->client: 返回接口內(nèi)容
整個流程是不是很清晰沃琅,同時jwt提供了一系列的接口包含了,生成token选酗、注銷token阵难、有效期設(shè)置等等常用的功能。下面我們利用jwt來實現(xiàn)用戶token的生成和校驗功能芒填,這里我們通過全局?jǐn)r截器來實現(xiàn)token的校驗呜叫,這樣可以做到對業(yè)務(wù)功能的無感。
首先我們要定義用戶的注冊和登錄的相關(guān)操作殿衰,文章中就不詳細(xì)講解了朱庆,代碼很簡單,具體大家可以參考源碼的modules.module1模塊中的相關(guān)業(yè)務(wù)代碼闷祥。
接下來就是我們具體集成jwt了娱颊。
pom依賴
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
jwt二次封裝
這里我們對jwt做了簡單的封裝,主要封裝了token生成方法凯砍,我們這里的payload只有userid箱硕,大家可以可以根據(jù)自己項目需要定制payload,這個payload在后續(xù)會使用到悟衩,用來從線程中獲取userid來判斷用戶權(quán)限剧罩。還封裝了獲取token的Claims對象的方法,Claims大家可以理解為jwt明文的報文體結(jié)構(gòu)座泳,在驗證的時候可以通過獲取到當(dāng)前請求的用戶payload信息惠昔。
@ConfigurationProperties(prefix = "mk.jwt")
@Component
public class JwtUtils {
private Logger logger = LoggerFactory.getLogger(getClass());
private String secret;
private long expire;
private String header;
/**
* 生成jwt token
*/
public String generateToken(long userId) {
Date nowDate = new Date();
//過期時間
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(userId+"")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Claims getClaimByToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
logger.debug("validate is token error ", e);
return null;
}
}
/**
* token是否過期
* @return true:過期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
//...省略setter、getter
}
定義攔截器
最后就是定義攔截器了挑势,讓自定義攔截器繼承自HandlerInterceptorAdapter這個攔截器適配器類镇防,當(dāng)然你也可以直接實現(xiàn)HandlerInterceptor接口,使用適配器類的話只需要實現(xiàn)自己關(guān)心的preHandle還是postHandle就可以了潮饱,這里我們只需要實現(xiàn)prehandle来氧,在controller和servlet處理之前攔截就可以了。
代碼注釋比較完善香拉,大家看注釋就可以了饲漾。
@Component
public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
@Autowired
private JwtUtils jwtUtils;
public static final String USER_KEY = "userId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Login annotation;
if(handler instanceof HandlerMethod) {
annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class);
}else{
return true;
}
if(annotation == null){
return true;
}
//獲取用戶憑證
String token = request.getHeader(jwtUtils.getHeader());
if(StringUtils.isBlank(token)){
token = request.getParameter(jwtUtils.getHeader());
}
//憑證為空
if(StringUtils.isBlank(token)){
throw new MkException(jwtUtils.getHeader() + "不能為空", HttpStatus.UNAUTHORIZED.toString());
}
Claims claims = jwtUtils.getClaimByToken(token);
if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
throw new MkException(jwtUtils.getHeader() + "失效,請重新登錄", HttpStatus.UNAUTHORIZED.toString());
}
//設(shè)置userId到request里缕溉,后續(xù)根據(jù)userId考传,獲取用戶信息
request.setAttribute(USER_KEY, Long.parseLong(claims.getSubject()));
return true;
}
}
定義好了攔截器,我們還需要在啟動的時候注冊上攔截器证鸥,我們還是在之前定義WebMvcConfig中進(jìn)行注冊攔截器僚楞,如果對指定的path進(jìn)行攔截勤晚,那么也可以在這里配置。
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Autowired
private DesParamsHandlerMethodArgumentResolver desParamsHandlerMethodArgumentResolver;
@Autowired
private AuthorizationInterceptor authorizationInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authorizationInterceptor).addPathPatterns("/**");
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(desParamsHandlerMethodArgumentResolver);
}
}
驗證
驗證就比較簡單了泉褐,注冊赐写、登錄的controller方法都在TestUserController中,大家可以先訪問register注冊一個用戶膜赃,再調(diào)用login獲取到該用戶的token挺邀,再將這個token放到queryUser接口的header中去訪問就可以獲取到該用戶的信息了,如果在header中沒有token或者token錯誤都會報錯跳座,并且也獲取不到別人的信息端铛,只能獲取到這個token的用戶信息。
總結(jié)
本框架中用了jwt來實現(xiàn)token校驗疲眷,當(dāng)然大家也可以通過使用oauth來實現(xiàn)token的相關(guān)功能禾蚕,如果實現(xiàn)oauth的相關(guān)功能,大家可以使喲過spring security或者shiro狂丝,token數(shù)據(jù)可以緩存在redis集群里來保證性能和高可用换淆。
本文對應(yīng)的github tag為v0.5,可以通過連接下載https://github.com/feiweiwei/MkFramework4java/releases/tag/v0.5几颜,也可以通過git clone -b v0.5 https://github.com/feiweiwei/MkFramework4java.git