一直以來,Spring系列給人的感受都是快速,簡潔,好理解,易操做.但Security是一個特例,這個框架相比而言,首先就是複雜,其次是靈活性也不夠.好在因而Spring出的,所以與Spring配合比較好.而且在Spring的大力推廣和支持下,它仍然屹立在這裏.固然它也有本身的優勢,好比他與LDAP還有Oauth這些結構的集成,處理的也不錯.咱們今天主要從如下幾個方面來分享關於Security的知識:java
基礎使用web
與OAuth2.x的集成spring
在web應用的設計中,權限是一個繞不開的話題.而在web權限設計中,RBAC是最流行的設計思路了.(除了RABC,還有像Linux中的ACL權限設計).在RBAC這種設計思路的引導下,咱們能夠有不少種實現方式,從最簡單的一個過濾器開始,到Security或Shiro,甚至和其餘的第三方進行集成,都是沒有問題的.今天咱們就先來看看Spring Security怎麼使用.數據庫
咱們建立一個SpringBoot項目,而後引入spring-security,pom中的依賴以下所示:json
<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>
而後咱們添加一個測試的接口,以下所示:api
@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); } }
咱們啓動,就會發如今日誌裏,他給咱們生成了這樣的一段內容:tomcat
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,而密碼則就是我在上面的日誌中.當咱們完成登陸後,咱們就能夠正常使用咱們的接口了.服務器
如今咱們來對SpringSecurity進行自定義用戶名密碼配置,咱們建立一個application.yml,而後設置以下:
spring: security: user: name: zhangsan password: zhangsan123
而後重啓咱們的應用,咱們會發現,SpringBoot不在給咱們提供默認密碼了,而當咱們訪問咱們的接口的時候,咱們可使用新配置的zhangsan和zhangsan123進行登陸.這樣的配置主要是由SpringSecurity中的WebSecurityConfig來實現的,所以咱們也能夠將用戶名和密碼寫到那裏面,這裏就不給你們演示了.
但這種方式仍然不夠靈活,一般咱們都會考慮由咱們本身來定義用戶信息以及權限信息,其實用戶信息與權限信息也是權限框架關注的兩個主要點,這兩點也被稱爲認證及受權.所謂認證,通俗點來說,就是登陸校驗,肯定訪問用戶的憑據是否正確.所謂受權就是該合法用戶是否擁有對應的權限.
這是咱們就須要問一個問題,Spring Security如何處理url與權限的匹配,也就是說Spring Security他如何知道哪些url是能夠被公開訪問,哪些url登陸後能夠訪問,哪些還須要某些固定的權限才能夠訪問?
這些問題的答案就在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進行測試,就能夠獲得咱們預期的結果了.
如今咱們完成了用戶的認證,那受權如何處理呢?其實在咱們剛剛所展現出來的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訪問咱們的接口進行測試了.
咱們能夠看到,基本受權使用Spring Security是比較複雜的,咱們徹底能夠只用一個Filter加幾個自定義註解完成這項工做,這也是不少人在開發一個單體項目時所作的一件事兒.這裏之因此提到單體項目是由於在多個項目之間,若是咱們想要進行權限限制,就不能在這麼作了,尤爲是咱們將咱們的部分資源公開提供給其餘人使用的時候,這樣的一刀切權限可能就不太適合.
當多個應用之間共享權限的時候,咱們每每會關注兩個問題,第一是權限的安全性,第二,是權限的粒度.什麼意思呢?好比說咱們提供了一個資源管理服務器,裏面有照片、視頻、文檔,而後你們能夠往咱們的資源服務器上傳本身的資源.後來用戶又在另一個網站上須要上傳本身的頭像,但他不想用默認的頭像,想用本身在資源服務器上的照片,那麼此時,若是他將本身在資源服務器上的用戶名和密碼告訴那個網站的話,首先是不安全,那個網站能夠隨時訪問他的資源服務器,其次是權限太大,第三方網站不只僅能夠訪問他的照片,還可以訪問他的視頻和文檔,這就很危險了.爲了解決這類問題,OAuth就誕生了.他容許用戶進行部分受權.他的原理很簡單.
OAuth在資源服務器和第三方網站之間作了一個受權層,而後受權層負責針對客戶端進行權限的限制,包括權限的範圍,有效期,這樣客戶端由於沒法直接訪問資源服務器,從而保護了用戶的資源.
那麼他的運行流程呢,就是這樣的:
用戶打開客戶端之後,客戶端要求用戶給予受權。
用戶贊成給予客戶端受權。
客戶端使用上一步得到的受權,向認證服務器申請令牌。
認證服務器對客戶端進行認證之後,確認無誤,贊成發放令牌。
客戶端使用令牌,向資源服務器申請獲取資源。
資源服務器確認令牌無誤,贊成向客戶端開放資源。
在以上這6步中,最關鍵的一步就是2,用戶如何纔可以給客戶端受權呢?OAuth2.0提供了四中受權方式,分別爲:
受權碼模式
簡化模式
密碼模式
客戶端模式
受權碼模式(authorization code)是功能最完整、流程最嚴密的受權模式。它的特色就是經過客戶端的後臺服務器,與"服務提供商"的認證服務器進行互動。他的具體步驟以下:
用戶訪問客戶端,後者將前者導向認證服務器。
用戶選擇是否給予客戶端受權。
假設用戶給予受權,認證服務器將用戶導向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個受權碼。
客戶端收到受權碼,附上早先的"重定向URI",向認證服務器申請令牌。這一步是在客戶端的後臺的服務器上完成的,對用戶不可見。
認證服務器覈對了受權碼和重定向URI,確認無誤後,向客戶端發送訪問令牌(access token)和更新令牌(refresh token)。
簡化模式(implicit grant type)不經過第三方應用程序的服務器,直接在瀏覽器中向認證服務器申請令牌,跳過了"受權碼"這個步驟,所以得名。全部步驟在瀏覽器中完成,令牌對訪問者是可見的,且客戶端不須要認證。步驟以下:
客戶端將用戶導向認證服務器。
用戶決定是否給於客戶端受權。
假設用戶給予受權,認證服務器將用戶導向客戶端指定的"重定向URI",並在URI的Hash部分包含了訪問令牌。
瀏覽器向資源服務器發出請求,其中不包括上一步收到的Hash值。
資源服務器返回一個網頁,其中包含的代碼能夠獲取Hash值中的令牌。
瀏覽器執行上一步得到的腳本,提取出令牌。
瀏覽器將令牌發給客戶端。
密碼模式(Resource Owner Password Credentials Grant)中,用戶向客戶端提供本身的用戶名和密碼。客戶端使用這些信息,向"服務商提供商"索要受權。在這種模式中,用戶必須把本身的密碼給客戶端,可是客戶端不得儲存密碼。這一般用在用戶對客戶端高度信任的狀況下,好比客戶端是操做系統的一部分,或者由一個著名公司出品。而認證服務器只有在其餘受權模式沒法執行的狀況下,才能考慮使用這種模式。步驟以下:
用戶向客戶端提供用戶名和密碼。
客戶端將用戶名和密碼發給認證服務器,向後者請求令牌。
認證服務器確認無誤後,向客戶端提供訪問令牌。
客戶端模式(Client Credentials Grant)指客戶端以本身的名義,而不是以用戶的名義,向"服務提供商"進行認證。嚴格地說,客戶端模式並不屬於OAuth框架所要解決的問題。在這種模式中,用戶直接向客戶端註冊,客戶端以本身的名義要求"服務提供商"提供服務,其實不存在受權問題。步驟以下:
客戶端向認證服務器進行身份認證,並要求一個訪問令牌。
認證服務器確認無誤後,向客戶端提供訪問令牌。
如今咱們知道OAuth受權主要由三部分構成,首先是客戶端,而後是認證服務器,最後是咱們的資源服務器,以前SpringBoot一直將OAuth2.0認證放在了SpringSecurity下,但在SpringBoot2.0文檔中,Spring官方說再也不提供認證服務器,可是以前的仍然是可使用的,只是在未來會被移除.