一直以來,Spring系列給人的感覺都是快速,簡潔,好理解,易操作.但Security是一個特例,這個框架相比而言,首先就是複雜,其次是靈活性也不夠.好在於是Spring出的,因此與Spring配合比較好.並且在Spring的大力推廣和支持下,它仍然屹立在這裏.當然它也有自己的優點,比如他與LDAP還有Oauth這些結構的集成,處理的也不錯.我們今天主要從以下幾個方面來分享關於Security的知識:
-
基礎使用
-
與OAuth2.x的集成
1. Security的基礎使用
在web應用的設計中,權限是一個繞不開的話題.而在web權限設計中,RBAC是最流行的設計思路了.(除了RABC,還有像Linux中的ACL權限設計).在RBAC這種設計思路的引導下,我們可以有很多種實現方式,從最簡單的一個過濾器開始,到Security或Shiro,甚至和其他的第三方進行集成,都是沒有問題的.今天我們就先來看看Spring Security怎麼使用.
1.1 SpringBoot中Security的默認配置
我們創建一個SpringBoot項目,然後引入spring-security,pom中的依賴如下所示:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>
然後我們添加一個測試的接口,如下所示:
@RestController @RequestMapping("users/") public class UserInfoController { @GetMapping("hello") public String hello(){ return "HelloWorld"; } }
最後是我們的啓動類,其實啓動類並沒有任何改變:
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class,args); } }
我們啓動,就會發現在日誌裏,他給我們生成了這樣的一段內容:
2018-11-13 13:42:15.307 INFO 13084 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2018-11-13 13:42:15.620 INFO 13084 --- [ main] .s.s.UserDetailsServiceAutoConfiguration : Using generated security password: b74fd02a-0ad2-40ec-b6cd-3f2edfa015c1 2018-11-13 13:42:15.756 INFO 13084 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7f13811b, org.springframework.security.web.context.SecurityContextPersistenceFilter@22d7fd41, org.springframework.security.web.header.HeaderWriterFilter@4fc165f6, org.springframework.security.web.csrf.CsrfFilter@65514add, org.springframework.security.web.authentication.logout.LogoutFilter@3bc69ce9, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@1ca610a0, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@79980d8d, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@59fc6d05, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@1775c4e7, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@19fd43da, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@2785db06, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@78307a56, org.springframework.security.web.session.SessionManagementFilter@5a7df831, org.springframework.security.web.access.ExceptionTranslationFilter@750f64fe, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1f9d4b0e] 2018-11-13 13:42:15.878 INFO 13084 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2018-11-13 13:42:15.885 INFO 13084 --- [ main] top.lianmengtu.security.Application : Started Application in 4.07 seconds (JVM running for 4.73)
這裏給我們生成了一個密碼,它是做什麼的呢?我們現在來訪問我們的測試接口,然後就會有一個登錄窗口讓你登錄.這是怎麼回事兒呢?
http://localhost:8080/users/hello
這是因爲當我們添加了security模塊後,SpringBoot默認爲我們啓用了security的攔截,並且如果我們沒有配置默認的用戶名密碼的話,他就給我們生成了一個默認的用戶名user,而密碼則就是我在上面的日誌中.當我們完成登錄後,我們就可以正常使用我們的接口了.
1.2 SpringBoot中SpringSecurity的簡易配置
現在我們來對SpringSecurity進行自定義用戶名密碼配置,我們創建一個application.yml,然後設置如下:
spring: security: user: name: zhangsan password: zhangsan123
然後重啓我們的應用,我們會發現,SpringBoot不在給我們提供默認密碼了,而當我們訪問我們的接口的時候,我們可以使用新配置的zhangsan和zhangsan123進行登錄.這樣的配置主要是由SpringSecurity中的WebSecurityConfig來實現的,因此我們也可以將用戶名和密碼寫到那裏面,這裏就不給大家演示了.
1.3 Security的一些自定義實現
但這種方式仍然不夠靈活,通常我們都會考慮由我們自己來定義用戶信息以及權限信息,其實用戶信息與權限信息也是權限框架關注的兩個主要點,這兩點也被稱爲認證及授權.所謂認證,通俗點來講,就是登錄校驗,確定訪問用戶的憑據是否正確.所謂授權就是該合法用戶是否擁有對應的權限.
這是我們就需要問一個問題,Spring Security如何處理url與權限的匹配,也就是說Spring Security他如何知道哪些url是可以被公開訪問,哪些url登錄後可以訪問,哪些還需要某些固定的權限纔可以訪問?
1.3.1 用戶認證
這些問題的答案就在WebSecurityConfigurerAdapter裏,在這個Adapter裏有兩個configure函數,一個是configure(HttpSecurity http),主要作用是配置哪些url可以直接放過,哪些是需要登錄才能訪問的,另外一個是configure(AuthenticationManagerBuilder auth),這個函數主要是用來做用戶認證的,我們現在先來寫url的映射與攔截.如下所示:
package top.lianmengtu.security.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated(); } }
此時如果我們訪問我們的接口,就會出現403的場景,如下所示,有沒有很熟悉的感覺:
Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback. Wed Nov 14 17:58:11 CST 2018 There was an unexpected error (type=Forbidden, status=403). Access Denied
只是這樣還不行,因爲我們希望能夠放過一些接口,比如登錄,然後其他的希望讓用戶登錄之後能夠進行訪問.我們先來改造一下我們的url處理接口configure(HttpSecurity http).
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/auth/login").permitAll()//放過登錄接口 .and().formLogin().loginProcessingUrl("/auth/login")//指定登錄處理接口 .successHandler(loginSuccessHandler).failureHandler(loginFailHandler)//指定登錄成功與登錄失敗的處理器 .and().authorizeRequests().anyRequest().authenticated()//對其他接口的權限限制爲登錄後才能訪問 .and().csrf().disable();//禁用csrf攔截,如果使用restclient和postman測試,建議禁掉,要不然會出錯 }
此時就需要用到configure(AuthenticationManagerBuilder auth)這個函數了.我們可以透過這個函數注入一個UsersDetailsService,然後在UserDetailsService裏來進行準確的處理.此時完成的SecurityConfig如下所示:
package top.lianmengtu.security.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import top.lianmengtu.security.handler.LoginFailHandler; import top.lianmengtu.security.handler.LoginSuccessHandler; /** * @program test_security * @description * @author: Jacob.Li * @create: 2018-11-13 16:01 **/ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyUserDetailServiceImpl userDetailService; @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired private LoginFailHandler loginFailHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/auth/login").permitAll()//放過登錄接口 .and().formLogin().loginProcessingUrl("/auth/login")//指定登錄處理接口 .successHandler(loginSuccessHandler).failureHandler(loginFailHandler)//指定登錄成功與登錄失敗的處理器 .and().authorizeRequests().anyRequest().authenticated()//對其他接口的權限限制爲登錄後才能訪問 .and().csrf().disable();//禁用csrf攔截,如果使用restclient和postman測試,建議禁掉,要不然會出錯 } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
UserDetailsService的實現如下:
package top.lianmengtu.security.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import top.lianmengtu.security.users.model.UserInfo; import top.lianmengtu.security.users.service.IUserInfoService; @Component public class MyUserDetailServiceImpl implements UserDetailsService { @Autowired private IUserInfoService userInfoService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println("----Hello"); System.out.println("name:"+username); if(!username.equals("zhangsan")){ throw new UsernameNotFoundException("用戶名不對"); } UserInfo userInfo=userInfoService.loadByNickName(username); return User.withUsername(username).password(userInfo.getPassword()).roles("ADMIN").build(); } }
UserDetailsService裏有一個userInfoService,這個是我們臨時自定義的一個接口,我們可以在這個接口裏接入數據庫,這裏只是一個簡單的實現,如下所示:
package top.lianmengtu.security.users.service; import top.lianmengtu.security.users.model.UserInfo; public interface IUserInfoService { public UserInfo loadByNickName(String nickName); } package top.lianmengtu.security.users.service.impl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import top.lianmengtu.security.users.model.UserInfo; import top.lianmengtu.security.users.service.IUserInfoService; @Service public class UserInfoService implements IUserInfoService { @Autowired PasswordEncoder passwordEncoder; @Override public UserInfo loadByNickName(String nickName) { UserInfo userInfo=new UserInfo(); userInfo.setNickName("zhangsan"); userInfo.setPassword(passwordEncoder.encode("123456")); return userInfo; } }
那登錄成功或失敗的處理邏輯呢?我們在SecurityConfig裏添加了LoginSuccessHandler和LoginFailHandler,也只是一個簡單的實現,這裏僅供參考:
package top.lianmengtu.security.handler; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * @program test_security * @description * @author: Jacob.Li * @create: 2018-11-14 16:41 **/ @Component public class LoginFailHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { ObjectMapper objectMapper=new ObjectMapper(); httpServletResponse.setContentType("application/json;charset=UTF-8"); Map<String,String> result=new HashMap<>(); result.put("code","-1"); result.put("msg","用戶名/密碼錯誤,請重新登錄"); httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result)); } }
package top.lianmengtu.security.handler; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @program test_security * @description * @author: Jacob.Li * @create: 2018-11-14 14:01 **/ @Component public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { ObjectMapper objectMapper=new ObjectMapper(); httpServletResponse.setContentType("application/json;charset=UTF-8"); // 響應類型 httpServletResponse.getWriter().write(objectMapper.writeValueAsString("登錄驗證成功")); System.out.println("-----login successful:"+objectMapper.writeValueAsString(authentication.getDetails())); } }
然後我們啓用restclient進行測試,就可以得到我們預期的結果了.
1.3.2 用戶授權
現在我們完成了用戶的認證,那授權如何處理呢?其實在我們剛剛所展示出來的UserDetailsService裏,有一個roles,這裏描述的是用戶的角色,我們現在只需要做兩件事就可以了.第一件就是獲取當前請求的url及其需要的角色,第二件就是與當前用戶的角色進行比較並作出放行或者攔阻的操作.
在SpringSecurity中,進行url攔截的是FilterInvocationSecurityMetadataSource,這裏我們自定義一個MyFilterInvocationSecurityMetadataSource,主要用於拿到當前url所需要的角色信息,並將這個url對應的角色信息傳入到下一個組件AccessDecisionManager中然後與用戶所擁有的角色進行比較,如果url沒有找到或者url沒有角色信息,這裏添加了一個默認的登錄認證,也可以直接放行.代碼如下所示:
package top.lianmengtu.security.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.stereotype.Component; import top.lianmengtu.security.users.service.IAuthorityService; import java.util.Arrays; import java.util.Collection; import java.util.List; /** * @program test_security * @description * @author: Jacob.Li * @create: 2018-11-15 09:37 **/ @Component public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Autowired private IAuthorityService authorityService; //接入自定義的Service @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { String requestUrl=((FilterInvocation)object).getRequestUrl(); System.out.println("----->MyFilterInvocationSecurityMetadataSource:拿到了url:"+requestUrl); if(requestUrl.equals("/auth/login")){ //登錄接口,直接放過 return null; } List<String> roleList=authorityService.findRolesByUrl(requestUrl); if(roleList.size()>0){ String[] roleArray=new String[roleList.size()]; for (int i = 0; i < roleList.size(); i++) { roleArray[i]=roleList.get(i); } return SecurityConfig.createList(roleArray); } return SecurityConfig.createList("ROLE_LOGIN"); //其他接口設置爲登錄放行,也可以像登錄接口一樣直接放過 } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } }
在這個實現中我們注入了我們自己的AuthorityService,這裏實現的比較簡單,大家可以根據需要從自己的數據庫裏對數據進行查找,Service示例如下所示:
package top.lianmengtu.security.users.service; import java.util.List; public interface IAuthorityService { public List<String> findRolesByUrl(String url); } ------------------------------------------------------- package top.lianmengtu.security.users.service.impl; import org.springframework.stereotype.Service; import top.lianmengtu.security.users.service.IAuthorityService; import java.util.ArrayList; import java.util.List; @Service public class AuthorityServiceImpl implements IAuthorityService { @Override public List<String> findRolesByUrl(String url) { System.out.println("-----url:"+url); List<String> rolesList=new ArrayList<>(); rolesList.add("ADMIN"); rolesList.add("MANAGER"); return rolesList; } }
當FilterInvocationSecurityMetadataSource的操作完成之後,他會將這個角色列表傳入到AccessDecisionManager中,在其中與登錄用戶的角色進行比較,然後決定放過還是攔截,自定義AccessDecisionManager代碼如下所示:
package top.lianmengtu.security.config; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; import java.util.Collection; import java.util.Iterator; /** * @program test_security * @description * @author: Jacob.Li * @create: 2018-11-15 09:55 **/ @Component public class MyAccessDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { Iterator<ConfigAttribute> configAttributeIterator = configAttributes.iterator(); //獲取上個組件中傳過來的角色集合 while (configAttributeIterator.hasNext()){ ConfigAttribute configAttribute=configAttributeIterator.next(); String role=configAttribute.getAttribute(); if("ROLE_LOGIN".equals(role)){ //判斷是否需要具備登錄權限 if(authentication instanceof AnonymousAuthenticationToken){ throw new BadCredentialsException("未登陸"); } return; } Collection<? extends GrantedAuthority> currentUserAuthorities=authentication.getAuthorities();//獲取用戶的角色信息 for(GrantedAuthority grantedAuthority: currentUserAuthorities){ if(grantedAuthority.getAuthority().equals(role)){//如果用戶的角色信息包含了當前鏈接所需要的角色,則放行 return; } } } throw new AccessDeniedException("權限不足"); } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; } }
現在我們已經做好了用戶授權的操作,現在是以異常的形式來展現結果,我們可以對結果進行處理,將結果轉換爲我們期望的json形式然後返回給前臺,這是由AccessDeniedHandler來處理的,自定義AccessDeniedHandler的代碼如下所示:
package top.lianmengtu.security.handler; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; import java.util.Map; /** * @program test_security * @description * @author: Jacob.Li * @create: 2018-11-15 10:06 **/ @Component public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); httpServletResponse.setCharacterEncoding("UTF-8"); PrintWriter out = httpServletResponse.getWriter(); Map<String,String> result=new HashMap<>(); result.put("code","-1"); result.put("msg","權限不足"); ObjectMapper objectMapper=new ObjectMapper(); String resultString=objectMapper.writeValueAsString(result); out.write(resultString); out.flush(); out.close(); } }
截止到現在,我們的準備工作已經做完了,現在我們來進行最後一步整合的操作,我們修改一下我們自定義的那個WebSecurityConfigurerAdapter組件中的configure(HttpSecurity http)函數,來使我們的處理真正生效,代碼如下所示:
package top.lianmengtu.security.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import top.lianmengtu.security.handler.LoginFailHandler; import top.lianmengtu.security.handler.LoginSuccessHandler; import top.lianmengtu.security.handler.MyAccessDeniedHandler; /** * @program test_security * @description * @author: Jacob.Li * @create: 2018-11-13 16:01 **/ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyUserDetailServiceImpl userDetailService; @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired private LoginFailHandler loginFailHandler; @Autowired private MyAccessDeniedHandler myAccessDeniedHandler; @Autowired private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource; @Autowired private MyAccessDecisionManager myAccessDecisionManager; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {//這裏的ObjectPostProcessor可以完全挪出去,像其他的handler一樣 @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);//配置我們剛剛自定義好的FilterInvocationSecurityMetadataSource,來加載url所需要的角色 object.setAccessDecisionManager(myAccessDecisionManager);//配置我們剛剛自定義好的AccessDecisionManager,來進行用戶角色和url所需角色的對比 return object; } }) .antMatchers("/auth/login").permitAll()//放過登錄接口 .and().formLogin().loginProcessingUrl("/auth/login")//指定登錄處理接口 .successHandler(loginSuccessHandler).failureHandler(loginFailHandler)//指定登錄成功與登錄失敗的處理器 .and().authorizeRequests().anyRequest().authenticated()//對其他接口的權限限制爲登錄後才能訪問 .and().csrf().disable() .exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);//授權失敗的處理 } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
好了,我們的授權工作已經完成了,現在我們可以重新啓動我們的項目,然後可以用restClient訪問我們的接口進行測試了.
2. OAuth2.0
我們可以看到,基本授權使用Spring Security是比較複雜的,我們完全可以只用一個Filter加幾個自定義註解完成這項工作,這也是很多人在開發一個單體項目時所做的一件事兒.這裏之所以提到單體項目是因爲在多個項目之間,如果我們想要進行權限限制,就不能在這麼做了,尤其是我們將我們的部分資源公開提供給其他人使用的時候,這樣的一刀切權限可能就不太適合.
當多個應用之間共享權限的時候,我們往往會關注兩個問題,第一是權限的安全性,第二,是權限的粒度.什麼意思呢?比如說我們提供了一個資源管理服務器,裏面有照片、視頻、文檔,然後大家可以往我們的資源服務器上傳自己的資源.後來用戶又在另外一個網站上需要上傳自己的頭像,但他不想用默認的頭像,想用自己在資源服務器上的照片,那麼此時,如果他將自己在資源服務器上的用戶名和密碼告訴那個網站的話,首先是不安全,那個網站可以隨時訪問他的資源服務器,其次是權限太大,第三方網站不僅僅可以訪問他的照片,還能夠訪問他的視頻和文檔,這就很危險了.爲了解決這類問題,OAuth就誕生了.他允許用戶進行部分授權.他的原理很簡單.
OAuth在資源服務器和第三方網站之間做了一個授權層,然後授權層負責針對客戶端進行權限的限制,包括權限的範圍,有效期,這樣客戶端因爲無法直接訪問資源服務器,從而保護了用戶的資源.
那麼他的運行流程呢,就是這樣的:
-
用戶打開客戶端以後,客戶端要求用戶給予授權。
-
用戶同意給予客戶端授權。
-
客戶端使用上一步獲得的授權,向認證服務器申請令牌。
-
認證服務器對客戶端進行認證以後,確認無誤,同意發放令牌。
-
客戶端使用令牌,向資源服務器申請獲取資源。
-
資源服務器確認令牌無誤,同意向客戶端開放資源。
2.1 OAuth2.0授權模式
在以上這6步中,最關鍵的一步就是2,用戶如何才能夠給客戶端授權呢?OAuth2.0提供了四中授權方式,分別爲:
-
授權碼模式
-
簡化模式
-
密碼模式
-
客戶端模式
2.1.1授權碼模式
授權碼模式(authorization code)是功能最完整、流程最嚴密的授權模式。它的特點就是通過客戶端的後臺服務器,與"服務提供商"的認證服務器進行互動。他的具體步驟如下:
-
用戶訪問客戶端,後者將前者導向認證服務器。
-
用戶選擇是否給予客戶端授權。
-
假設用戶給予授權,認證服務器將用戶導向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個授權碼。
-
客戶端收到授權碼,附上早先的"重定向URI",向認證服務器申請令牌。這一步是在客戶端的後臺的服務器上完成的,對用戶不可見。
-
認證服務器覈對了授權碼和重定向URI,確認無誤後,向客戶端發送訪問令牌(access token)和更新令牌(refresh token)。
2.1.2 簡化模式
簡化模式(implicit grant type)不通過第三方應用程序的服務器,直接在瀏覽器中向認證服務器申請令牌,跳過了"授權碼"這個步驟,因此得名。所有步驟在瀏覽器中完成,令牌對訪問者是可見的,且客戶端不需要認證。步驟如下:
-
客戶端將用戶導向認證服務器。
-
用戶決定是否給於客戶端授權。
-
假設用戶給予授權,認證服務器將用戶導向客戶端指定的"重定向URI",並在URI的Hash部分包含了訪問令牌。
-
瀏覽器向資源服務器發出請求,其中不包括上一步收到的Hash值。
-
資源服務器返回一個網頁,其中包含的代碼可以獲取Hash值中的令牌。
-
瀏覽器執行上一步獲得的腳本,提取出令牌。
-
瀏覽器將令牌發給客戶端。
2.1.3 密碼模式
密碼模式(Resource Owner Password Credentials Grant)中,用戶向客戶端提供自己的用戶名和密碼。客戶端使用這些信息,向"服務商提供商"索要授權。在這種模式中,用戶必須把自己的密碼給客戶端,但是客戶端不得儲存密碼。這通常用在用戶對客戶端高度信任的情況下,比如客戶端是操作系統的一部分,或者由一個著名公司出品。而認證服務器只有在其他授權模式無法執行的情況下,才能考慮使用這種模式。步驟如下:
-
用戶向客戶端提供用戶名和密碼。
-
客戶端將用戶名和密碼發給認證服務器,向後者請求令牌。
-
認證服務器確認無誤後,向客戶端提供訪問令牌。
2.1.4 客戶端模式
客戶端模式(Client Credentials Grant)指客戶端以自己的名義,而不是以用戶的名義,向"服務提供商"進行認證。嚴格地說,客戶端模式並不屬於OAuth框架所要解決的問題。在這種模式中,用戶直接向客戶端註冊,客戶端以自己的名義要求"服務提供商"提供服務,其實不存在授權問題。步驟如下:
-
客戶端向認證服務器進行身份認證,並要求一個訪問令牌。
-
認證服務器確認無誤後,向客戶端提供訪問令牌。
2.2 SpringBoot中的OAuth2.0
現在我們知道OAuth授權主要由三部分構成,首先是客戶端,然後是認證服務器,最後是我們的資源服務器,之前SpringBoot一直將OAuth2.0認證放在了SpringSecurity下,但在SpringBoot2.0文檔中,Spring官方說不再提供認證服務器,但是之前的仍然是可以使用的,只是在將來會被移除.