一、JWT概述
JWT中主要包括三個部分:
1、頭部:包含簽名的加密算法和token類型。將這個json串用base64url進行編碼即形成了第一部分的token。
2、載荷:包括用戶id、用戶名、過期時間等,但是不包括用戶的敏感信息,因爲可以被反解出來。將這個json串用base64url進行編碼即形成了第二部分的token。
3、簽名:將前兩部分的密文用頭部指定的加密算法進行加鹽加密(必須保證這個鹽只有認證中心才知道),之後再用base64url編碼。
爲了保證鹽的私密性,可採用RSA非對稱加密方式。
RSA通俗理解:
如果是加密,那肯定是不希望別人知道我的消息,所以只有我才能解密,所以可得出公鑰負責加密,私鑰負責解密。
如果是簽名,那肯定是不希望有人冒充我發消息,只有我才能發佈這個簽名,所以可得出私鑰負責簽名,公鑰負責驗證。
二、SSO單點登錄
1、SSO概述
單點登錄,簡稱SSO,說到底還是分佈式認證,即我們常說的指的是在多應用系統的項目中,用戶只需要登錄一次,就可以訪問所有互相信任的應用系統。
SSO實現起來比較簡單,從分佈式認證流程中,起到最關鍵作用的就是token,token的安全與否,直接關係到系統的健壯性,所以我們可以使用成熟的JWT來實現token的生成和校驗。
2、流程梳理
網上找的單點登錄流程圖:
其實上面的流程圖還是比較複雜的,需要客戶端一直保持和認證中心的全局會話,如果使用JWT的話,可以簡化相應步驟爲下圖(因爲JWT的載荷部分已經存儲了過期時間,只要其他關聯繫統存儲這相同的JWT解析規則就沒必要一直和認證中心保持着全局會話了):
3、JWT+SpringSecurity單點登錄解決步驟
先使用RSA生成一套公鑰和私鑰,私鑰只保存在認證中心處。之前用過SoringSecurity的可以直接它的的過濾器鏈,登陸的時候往認證中心發送用戶名、密碼,成功認證後,不僅給出用戶的角色信息,還將JWT生成的token令牌放入響應頭中。之後用戶每次發請求都帶上這個token,JWT解析規則由認證中心及關聯的系統共享,其實這就已經完成了單點登錄,還是很簡單的。
編寫認證中心的認證過濾器。
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private RsaKeyProperties prop;
public JwtLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
this.authenticationManager = authenticationManager;
this.prop = prop;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
SysUser user = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
//繼續調用自定義的UserDetailsService進行判斷
return authenticationManager.authenticate(authRequest);
} catch (Exception e) {
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED);
resultMap.put("msg", "用戶名或密碼錯誤");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
return null;
}
@Override
public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SysUser user = new SysUser();
user.setUsername(authResult.getName());
user.setRoles((List<SysRole>) authResult.getAuthorities());
//密碼不能放入token中
//私鑰加密
String token = JwtUtils.generateToken(user, prop.getPrivateKey(), 1000 * 60 * 30);//半小時後過期
//生成token並放到響應的消息頭中
response.addHeader("Authorization", "Bearer " + token);
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("code", HttpServletResponse.SC_OK);
resultMap.put("msg", "登錄成功");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
編寫校驗過濾器。
public class JwtVerifyFilter extends BasicAuthenticationFilter {
private RsaKeyProperties prop;
public JwtVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
super(authenticationManager);
this.prop = prop;
}
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader("Authorization");
if (StringUtils.isEmpty(header) || !header.startsWith("Bearer ")) {
//攜帶錯誤格式的token
chain.doFilter(request, response);
responseJson(response);
return;
} else {
//token格式正確, 但還需要校驗token的正確性
String token = header.replace("Bearer ", "");
try {
Payload<SysUser> payload = JwtUtils.parseToken(token, prop.getPublicKey(), SysUser.class);
SysUser user = JsonUtils.toBean(JsonUtils.toString(payload.getUserInfo()), SysUser.class);
UsernamePasswordAuthenticationToken authResult = null;
if (user != null) {
//因爲在生成token的時候沒有傳password所以這裏第二個參數爲空, 第三個參數是認證成功後的角色信息
authResult = new UsernamePasswordAuthenticationToken(user.getUsername(), null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
} else {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED);
resultMap.put("msg", "認證失敗");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
}
} catch (Exception ex) {
responseJson(response);
}
}
}
private void responseJson(HttpServletResponse response) throws IOException {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED);
resultMap.put("msg", "請登錄");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
}
}
三、OAuth2
OAuth是Open Authorization的簡寫。 OAuth協議爲用戶資源的授權提供了一個安全的、開放而又簡易的標準,其實就是允許用戶提供一個令牌,而不是用戶名和密碼來訪問他們存放在特定服務提供者的數據。每一個令牌授權一個特定的網站。
舉個例子。
A網站是一個打印照片的網站,B網站是一個存儲照片的網站,二者原本毫無關聯。
如果一個用戶想使用A網站打印自己存儲在B網站的照片,那麼A網站就需要使用B網站的照片資源才行。 按照傳統的思考模式,我們需要A網站具有登錄B網站的用戶名和密碼才行,但是,現在有了OAuth2,只需要A網站獲取到使用B網站照片資源的一個通行令牌即可!這個令牌無需具備操作B網站所有資源的權限,也無需永久有效,只要滿足A網站打印照片需求即可。
這麼說和單點登錄有一點點像?其實還是不同的。
單點登錄是用戶一次登錄,自己可以操作其他關聯的服務資源。
OAuth2則是用戶給一個系統授權,可以直接操作其他系統資源的一種方式。
直接上OAuth2的授權碼模式流程圖,圖說的很形象了。
配合文字看圖。
用戶登錄A系統進行照片打印,但是打印的照片呢存儲在B系統上面,A系統需要提供一個重定向的URL,B系統作爲OAuth2的資源服務。既然用戶想間接訪問系統B,那勢必要有B系統的權限,A系統的客戶端信息存儲到OAuth2的認證服務中,進行認證之後B系統返回一個認證碼並重定向到之前A系統提供的URL,A系統再通過這個認證碼和自己在OAuth2認證服務上存儲的客戶端信息發送給OAuth2的認證服務端,服務端發放通行令牌給A系統。OK,此時A系統就能打印存儲在B系統上的資源了。