使用Jwt生成token解決SSO單點登錄以及OAuth2資源權限管理

一、JWT概述

       JWT中主要包括三個部分:

       1、頭部:包含簽名的加密算法和token類型。將這個json串用base64url進行編碼即形成了第一部分的token。

       2、載荷:包括用戶id、用戶名、過期時間等,但是不包括用戶的敏感信息,因爲可以被反解出來。將這個json串用base64url進行編碼即形成了第二部分的token。

       3、簽名:將前兩部分的密文用頭部指定的加密算法進行加鹽加密(必須保證這個鹽只有認證中心才知道),之後再用base64url編碼。

       爲了保證鹽的私密性,可採用RSA非對稱加密方式


RSA通俗理解:
       如果是加密,那肯定是不希望別人知道我的消息,所以只有我才能解密,所以可得出公鑰負責加密,私鑰負責解密。
       如果是簽名,那肯定是不希望有人冒充我發消息,只有我才能發佈這個簽名,所以可得出私鑰負責簽名,公鑰負責驗證。




二、SSO單點登錄

1、SSO概述

       單點登錄,簡稱SSO,說到底還是分佈式認證,即我們常說的指的是在多應用系統的項目中,用戶只需要登錄一次,就可以訪問所有互相信任的應用系統

Alt

       SSO實現起來比較簡單,從分佈式認證流程中,起到最關鍵作用的就是token,token的安全與否,直接關係到系統的健壯性,所以我們可以使用成熟的JWT來實現token的生成和校驗。



2、流程梳理

       網上找的單點登錄流程圖:
Alt

       其實上面的流程圖還是比較複雜的,需要客戶端一直保持和認證中心的全局會話,如果使用JWT的話,可以簡化相應步驟爲下圖(因爲JWT的載荷部分已經存儲了過期時間,只要其他關聯繫統存儲這相同的JWT解析規則就沒必要一直和認證中心保持着全局會話了):
Alt



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網站打印照片需求即可。

Alt



這麼說和單點登錄有一點點像?其實還是不同的。

單點登錄是用戶一次登錄,自己可以操作其他關聯的服務資源。
OAuth2則是用戶給一個系統授權,可以直接操作其他系統資源的一種方式。



直接上OAuth2的授權碼模式流程圖,圖說的很形象了。
Alt
       配合文字看圖。
       用戶登錄A系統進行照片打印,但是打印的照片呢存儲在B系統上面,A系統需要提供一個重定向的URL,B系統作爲OAuth2的資源服務。既然用戶想間接訪問系統B,那勢必要有B系統的權限,A系統的客戶端信息存儲到OAuth2的認證服務中,進行認證之後B系統返回一個認證碼重定向到之前A系統提供的URLA系統再通過這個認證碼和自己在OAuth2認證服務上存儲的客戶端信息發送給OAuth2的認證服務端,服務端發放通行令牌給A系統。OK,此時A系統就能打印存儲在B系統上的資源了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章