SpringBoot 整合 oauth2(三)實現 token 認證

內容導讀

關於session和token的使用,網上爭議一直很大。

總的來說爭議在這裡:

  1. session是空間換時間,而token是時間換空間。session佔用空間,但是可以管理過期時間,token管理部了過期時間,但是不佔用空間.
  2. sessionId失效問題和token內包含。
  3. session基於cookie,app請求並沒有cookie 。
  4. token更加安全(每次請求都需要帶上)。

第一章順風車:

第二章順風車:

開始正文了...

本文大概流程:

oauth2流程簡介

詳情請參考文檔:

在oauth2協議裡,每一個應用都有自己的一個clientId和clientSecret(需要去認證方申請),所以一旦想通過認證,必須要有認證方下發的clientId和secret。

SpringBoot 整合 oauth2(三)實現 token 認證

原理這塊確實很麻煩,希望不理解的多看看參考文檔。

1. pom

   org.springframework.boot spring-boot-starter-security   org.springframework.security.oauth spring-security-oauth2 

2. 項目架構介紹

SpringBoot 整合 oauth2(三)實現 token 認證

我們需要這七個類來完成。

3. UserDetail實現認證第一步

MyUserDetailsService.java

/** * Created by Fant.J. */@Componentpublic class MyUserDetailsService implements UserDetailsService { @Reference(version = "2.0.0") private UserService userService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { String passwd = ""; System.out.println("收到的賬號"+username); if (CheckFormat.isEmail(username)){ passwd = userService.selectPasswdByEmail(username); }else if (CheckFormat.isPhone(username)){ passwd = userService.selectPasswdByPhone(username); }else { throw new RuntimeException("登錄賬號不存在"); } System.out.println("查到的密碼"+passwd); return new User(username, passwd, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER")); }}

這裡重寫了security認證UserDetailsService 的接口方法,添加了自定義數據庫密碼的查詢和校驗。

為什麼我把它放在了controller包了呢,因為我用的dubbo,@Reference註解掃描包是controller。這注解在別的包下失效。沒搞過dubbo的朋友就把它當作是調用service層就行。

4. 認證成功/失敗處理器

這部分在security的 就有,這裡稍有改動。

改動一:去掉了返回view還是json的判斷,統一返回json。

改動二:修改登陸成功處理器,添加oauth2 客戶端的認證。

MyAuthenticationFailHandler.java

/** * 自定義登錄失敗處理器 * Created by Fant.J. */@Componentpublic class MyAuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { logger.info("登錄失敗"); //設置狀態碼 response.setStatus(500); response.setContentType("application/json;charset=UTF-8"); //將 登錄失敗 信息打包成json格式返回 response.getWriter().write(JSON.toJSONString(ServerResponse.createByErrorMessage(exception.getMessage()))); }}

MyAuthenticationSuccessHandler.java

/** * Created by Fant.J. */@Componentpublic class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private ClientDetailsService clientDetailsService; @Autowired private AuthorizationServerTokenServices authorizationServerTokenServices; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { logger.info("登錄成功"); String header = request.getHeader("Authorization"); if (header == null && !header.startsWith("Basic")) { throw new UnapprovedClientAuthenticationException("請求投中無client信息"); } String[] tokens = this.extractAndDecodeHeader(header, request); assert tokens.length == 2; //獲取clientId 和 clientSecret String clientId = tokens[0]; String clientSecret = tokens[1]; //獲取 ClientDetails ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId); if (clientDetails == null){ throw new UnapprovedClientAuthenticationException("clientId 不存在"+clientId); //判斷 方言 是否一致 }else if (!StringUtils.equals(clientDetails.getClientSecret(),clientSecret)){ throw new UnapprovedClientAuthenticationException("clientSecret 不匹配"+clientId); } //密碼授權 模式, 組建 authentication TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP,clientId,clientDetails.getScope(),"password"); OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails); OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request,authentication); OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication); //判斷是json 格式返回 還是 view 格式返回 //將 authention 信息打包成json格式返回 response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(JSON.toJSONString(ServerResponse.createBySuccess(token))); } /** * 解碼請求頭 */ private String[] extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException { byte[] base64Token = header.substring(6).getBytes("UTF-8"); byte[] decoded; try { decoded = Base64.decode(base64Token); } catch (IllegalArgumentException var7) { throw new BadCredentialsException("Failed to decode basic authentication token"); } String token = new String(decoded, "UTF-8"); int delim = token.indexOf(":"); if (delim == -1) { throw new BadCredentialsException("Invalid basic authentication token"); } else { return new String[]{token.substring(0, delim), token.substring(delim + 1)}; } }}

描述:

從request的請求頭中拿到Authorization信息,根據clientId獲取到secret和請求頭中的secret信息做對比,如果正確,組建一個新的TokenRequest類,然後根據前者和clientDetails創建OAuth2Request對象,然後根據前者和authentication創建OAuth2Authentication對象。最後通過AuthorizationServerTokenServices和前者前者創建OAuth2AccessToken對象。然後將token返回。

提示:

  1. 請求頭:Authorization -> Basic aW50ZXJuZXRfcGx1czppbnRlcm5ldF9wbHVz 。注意是Basic 開頭然後是clientid:clientScret 格式進行base64加密後的字符串。
  2. 請求參數:username和password是必須要的參數,值對應的就是賬號密碼,還有給可有可無的就是scope,它來聲明該用戶有多大的權限,默認是all。grant_type也是默認的參數,默認是password,它表示你以哪種認證模式來認證。
SpringBoot 整合 oauth2(三)實現 token 認證

這是請求頭解密現場

5. 配置認證/資源服務器

MyResourceServerConfig.java

/** * 資源服務器 * Created by Fant.J. */@Configuration@EnableResourceServerpublic class MyResourceServerConfig extends ResourceServerConfigurerAdapter { @Autowired private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowired private MyAuthenticationFailHandler myAuthenticationFailHandler; @Override public void configure(HttpSecurity http) throws Exception { //表單登錄 方式 http.formLogin() .loginPage("/authentication/require") //登錄需要經過的url請求 .loginProcessingUrl("/authentication/form") .successHandler(myAuthenticationSuccessHandler) .failureHandler(myAuthenticationFailHandler); http .authorizeRequests() .antMatchers("/user/*") .authenticated() .antMatchers("/oauth/token").permitAll() .anyRequest() .permitAll() .and() //關閉跨站請求防護 .csrf().disable(); }}

我這裡只需要認證/user/*開頭的url。@EnableResourceServer這個註解就決定了這十個資源服務器。它決定了哪些資源需要什麼樣的權限。

MyAuthorizationServerConfig.java

/** * 認證服務器 * Created by Fant.J. */@Configuration@EnableAuthorizationServerpublic class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { super.configure(security); } /** * 客戶端配置(給誰發令牌) * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("internet_plus") .secret("internet_plus") //有效時間 2小時 .accessTokenValiditySeconds(72000) //密碼授權模式和刷新令牌 .authorizedGrantTypes({"refresh_token","password"}) .scopes( "all"); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .authenticationManager(authenticationManager) .userDetailsService(userDetailsService); } }}

@EnableAuthorizationServer就代表了它是個認證服務端。

一般來講,認證服務器是第三方提供的服務,比如你想接入qq登陸接口,那麼認證服務器就是騰訊提供,然後你在本地做資源服務,但是認證和資源服務不是非要物理上的分離,只需要做到邏輯上的分離就好。

執行

好了,啟動項目。

1. 給/oauth/token 發送post請求獲取token

請求頭:Authorization:Basic +clientid:secret 的base64加密字符串 (認證服務器中設置的client信息)

請求參數:username password (用戶登陸賬號密碼)

{ "data": { "refreshToken": { "expiration": 1528892642111, "value": "xxxxxx-xxxxxx-xxxxx-xxxxxxxx" }, "scope": [ "all" ], "tokenType": "bearer", "value": "xxxxxx-xxxxxx-xxxxx-xxxxxxxx" }, "status": 200, "success": true}
2. 給/oauth/token 發送post請求刷新token

請求頭: 不需要

請求參數:

  1. grant_type:refresh_token
  2. refresh_token:獲取token時返回的refreshToken的value
3. 訪問受保護的資源,比如/user/2

類型:get請求

請求頭:Authorization: bearer + tokenValue

SpringBoot 整合 oauth2(三)實現 token 認證

介紹下我的所有文集:

流行框架

底層實現原理:


分享到:


相關文章: