0.前言html
通過前面一小節已經基本配置好了基於SpringBoot+SpringSecurity+OAuth2.0的環境。這一小節主要對一些寫固定InMemory的User和Client進行擴展。實現動態查詢用戶,但爲了演示方便,這裏沒有查詢數據庫。僅作Demo演示,最最關鍵的是,做爲我我的筆記。其實代碼裏面有些註釋,可能只有我知道爲何,有些是Debug調試時的一些測試代碼。仍是建議,讀者本身跑一遍會比較好,能跟深刻的理解OAuth2.0協議。我也是參考網上不少博客,而後慢慢測試和理解的。
參考的每一個人的博客,都寫得很好很仔細,可是有些關鍵點,仍是要本身寫個Demo出來纔會更好理解。
結合數據庫的,期待下一篇博客java
1.目錄結構git
SecurityConfiguration.java Spring-Security 配置
auth/BaseClientDetailService.java 自定義客戶端認證
auth/BaseUserDetailService.java 自定義用戶認證
integration/* 經過過濾器方式對OAuth2.0集成多種認證方式
model/SysGrantedAuthority.java 受權權限模型
model/SysUserAuthentication.java 認證用戶主體模型
server/AuthorizationServerConfiguration.java OAuth 受權服務器配置
server/ResourceServerConfiguration.java OAuth 資源服務器配置github
2.代碼解析redis
(1) SecurityConfiguration.javaspring
1 /** 2 * Spring-Security 配置<br> 3 * 具體參考: https://github.com/lexburner/oauth2-demo 4 * http://blog.didispace.com/spring-security-oauth2-xjf-1/ 5 * https://www.cnblogs.com/cjsblog/p/9152455.html 6 * https://segmentfault.com/a/1190000014371789 (多種認證方式) 7 * @author wunaozai 8 * @date 2018-05-28 9 */ 10 @Configuration 11 @EnableWebSecurity 12 @EnableGlobalMethodSecurity(prePostEnabled = true) //啓用方法級的權限認證 13 public class SecurityConfiguration extends WebSecurityConfigurerAdapter { 14 15 //經過自定義userDetailsService 來實現查詢數據庫,手機,二維碼等多種驗證方式 16 @Bean 17 @Override 18 protected UserDetailsService userDetailsService(){ 19 //採用一個自定義的實現UserDetailsService接口的類 20 return new BaseUserDetailService(); 21 /* 22 InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); 23 BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); 24 String finalPassword = "{bcrypt}"+bCryptPasswordEncoder.encode("123456"); 25 manager.createUser(User.withUsername("user_1").password(finalPassword).authorities("USER").build()); 26 finalPassword = "{noop}123456"; 27 manager.createUser(User.withUsername("user_2").password(finalPassword).authorities("USER").build()); 28 return manager; 29 */ 30 } 31 32 @Override 33 protected void configure(HttpSecurity http) throws Exception { 34 // http.authorizeRequests() 35 // .antMatchers("/", "/index.html", "/oauth/**").permitAll() //容許訪問 36 // .anyRequest().authenticated() //其餘地址的訪問須要驗證權限 37 // .and() 38 // .formLogin() 39 // .loginPage("/login.html") //登陸頁 40 // .failureUrl("/login-error.html").permitAll() 41 // .and() 42 // .logout() 43 // .logoutSuccessUrl("/index.html"); 44 http.authorizeRequests().anyRequest().fullyAuthenticated(); 45 http.formLogin().loginPage("/login").failureUrl("/login?code=").permitAll(); 46 http.logout().permitAll(); 47 http.authorizeRequests().antMatchers("/oauth/authorize").permitAll(); 48 } 49 50 /** 51 * 用戶驗證 52 */ 53 @Override 54 protected void configure(AuthenticationManagerBuilder auth) throws Exception { 55 super.configure(auth); 56 } 57 58 /** 59 * Spring Boot 2 配置,這裏要bean 注入 60 */ 61 @Bean 62 @Override 63 public AuthenticationManager authenticationManagerBean() throws Exception { 64 AuthenticationManager manager = super.authenticationManagerBean(); 65 return manager; 66 } 67 68 @Bean 69 PasswordEncoder passwordEncoder() { 70 return PasswordEncoderFactories.createDelegatingPasswordEncoder(); 71 } 72 }
(2) AuthorizationServerConfiguration.java數據庫
1 /** 2 * OAuth 受權服務器配置 3 * https://segmentfault.com/a/1190000014371789 4 * @author wunaozai 5 * @date 2018-05-29 6 */ 7 @Configuration 8 @EnableAuthorizationServer 9 public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { 10 11 private static final String DEMO_RESOURCE_ID = "order"; 12 13 @Autowired 14 AuthenticationManager authenticationManager; 15 @Autowired 16 RedisConnectionFactory redisConnectionFactory; 17 18 @Override 19 public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 20 //String finalSecret = "{bcrypt}"+new BCryptPasswordEncoder().encode("123456"); 21 //clients.setBuilder(builder); 22 //這裏經過實現 ClientDetailsService接口 23 clients.withClientDetails(new BaseClientDetailService()); 24 /* 25 //配置客戶端,一個用於password認證一個用於client認證 26 clients.inMemory() 27 .withClient("client_1") 28 .resourceIds(DEMO_RESOURCE_ID) 29 .authorizedGrantTypes("client_credentials", "refresh_token") 30 .scopes("select") 31 .authorities("oauth2") 32 .secret(finalSecret) 33 .and() 34 .withClient("client_2") 35 .resourceIds(DEMO_RESOURCE_ID) 36 .authorizedGrantTypes("password", "refresh_token") 37 .scopes("select") 38 .authorities("oauth2") 39 .secret(finalSecret) 40 .and() 41 .withClient("client_code") 42 .resourceIds(DEMO_RESOURCE_ID) 43 .authorizedGrantTypes("authorization_code", "client_credentials", "refresh_token", 44 "password", "implicit") 45 .scopes("all") 46 //.authorities("oauth2") 47 .redirectUris("http://www.baidu.com") 48 .accessTokenValiditySeconds(1200) 49 .refreshTokenValiditySeconds(50000); 50 */ 51 } 52 53 @Override 54 public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { 55 endpoints 56 .tokenStore(new RedisTokenStore(redisConnectionFactory)) 57 .authenticationManager(authenticationManager) 58 .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); 59 60 //配置TokenService參數 61 DefaultTokenServices tokenService = new DefaultTokenServices(); 62 tokenService.setTokenStore(endpoints.getTokenStore()); 63 tokenService.setSupportRefreshToken(true); 64 tokenService.setClientDetailsService(endpoints.getClientDetailsService()); 65 tokenService.setTokenEnhancer(endpoints.getTokenEnhancer()); 66 tokenService.setAccessTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(30)); //30天 67 tokenService.setRefreshTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(50)); //50天 68 tokenService.setReuseRefreshToken(false); 69 endpoints.tokenServices(tokenService); 70 71 } 72 73 @Override 74 public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { 75 //容許表單認證 76 //這裏增長攔截器到安全認證鏈中,實現自定義認證,包括圖片驗證,短信驗證,微信小程序,第三方系統,CAS單點登陸 77 //addTokenEndpointAuthenticationFilter(IntegrationAuthenticationFilter()) 78 //IntegrationAuthenticationFilter 採用 @Component 注入 79 oauthServer.allowFormAuthenticationForClients() 80 .tokenKeyAccess("isAuthenticated()") 81 .checkTokenAccess("permitAll()"); 82 } 83 84 }
(3) ResourceServerConfiguration.java小程序
1 /** 2 * OAuth 資源服務器配置 3 * @author wunaozai 4 * @date 2018-05-29 5 */ 6 @Configuration 7 @EnableResourceServer 8 public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { 9 10 private static final String DEMO_RESOURCE_ID = "order"; 11 12 @Override 13 public void configure(ResourceServerSecurityConfigurer resources) { 14 resources.resourceId(DEMO_RESOURCE_ID).stateless(true); 15 } 16 17 @Override 18 public void configure(HttpSecurity http) throws Exception { 19 // Since we want the protected resources to be accessible in the UI as well we need 20 // session creation to be allowed (it's disabled by default in 2.0.6) 21 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) 22 .and() 23 .requestMatchers().anyRequest() 24 .and() 25 .anonymous() 26 .and() 27 // .authorizeRequests() 28 // .antMatchers("/order/**").authenticated();//配置order訪問控制,必須認證事後才能夠訪問 29 .authorizeRequests() 30 .antMatchers("/order/**").hasAuthority("admin_role");//配置訪問控制,必須具備admin_role權限才能夠訪問資源 31 // .antMatchers("/order/**").hasAnyRole("admin"); 32 } 33 34 }
(4) BaseClientDetailService.javasegmentfault
1 /** 2 * 自定義客戶端認證 3 * @author wunaozai 4 * @date 2018-06-20 5 */ 6 public class BaseClientDetailService implements ClientDetailsService { 7 8 private static final Logger log = LoggerFactory.getLogger(BaseClientDetailService.class); 9 10 @Override 11 public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException { 12 System.out.println(clientId); 13 BaseClientDetails client = null; 14 //這裏能夠改成查詢數據庫 15 if("client".equals(clientId)) { 16 log.info(clientId); 17 client = new BaseClientDetails(); 18 client.setClientId(clientId); 19 client.setClientSecret("{noop}123456"); 20 //client.setResourceIds(Arrays.asList("order")); 21 client.setAuthorizedGrantTypes(Arrays.asList("authorization_code", 22 "client_credentials", "refresh_token", "password", "implicit")); 23 //不一樣的client能夠經過 一個scope 對應 權限集 24 client.setScope(Arrays.asList("all", "select")); 25 client.setAuthorities(AuthorityUtils.createAuthorityList("admin_role")); 26 client.setAccessTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(1)); //1天 27 client.setRefreshTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(1)); //1天 28 Set<String> uris = new HashSet<>(); 29 uris.add("http://localhost:8080/login"); 30 client.setRegisteredRedirectUri(uris); 31 } 32 if(client == null) { 33 throw new NoSuchClientException("No client width requested id: " + clientId); 34 } 35 return client; 36 } 37 38 }
(5) BaseUserDetailService.java微信小程序
1 /** 2 * 自定義用戶認證Service 3 * @author wunaozai 4 * @date 2018-06-19 5 */ 6 //@Service 7 public class BaseUserDetailService implements UserDetailsService { 8 9 private static final Logger log = LoggerFactory.getLogger(BaseUserDetailService.class); 10 11 @Override 12 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 13 log.info(username); 14 System.out.println(username); 15 //return new User(username, "{noop}123456", false, false, null); 16 //User user = null; 17 SysUserAuthentication user = null; 18 if("admin".equals(username)) { 19 IntegrationAuthentication auth = IntegrationAuthenticationContext.get(); 20 //這裏能夠經過auth 獲取 user 值 21 //而後根據當前登陸方式type 而後建立一個sysuserauthentication 從新設置 username 和 password 22 //好比使用手機驗證碼登陸的, username就是手機號 password就是6位的驗證碼{noop}000000 23 System.out.println(auth); 24 List<GrantedAuthority> list = AuthorityUtils.createAuthorityList("admin_role"); //所謂的角色,只是增長ROLE_前綴 25 user = new SysUserAuthentication(); 26 user.setUsername(username); 27 user.setPassword("{noop}123456"); 28 user.setAuthorities(list); 29 user.setAccountNonExpired(true); 30 user.setAccountNonLocked(true); 31 user.setCredentialsNonExpired(true); 32 user.setEnabled(true); 33 34 //user = new User(username, "{noop}123456", list); 35 log.info("---------------------------------------------"); 36 log.info(user.toJSONString()); 37 log.info("---------------------------------------------"); 38 //這裏會根據user屬性拋出鎖定,禁用等異常 39 } 40 41 return user;//返回UserDetails的實現user不爲空,則驗證經過 42 } 43 }
(6) SysGrantedAuthority.java
1 /** 2 * 受權權限模型 3 * @author wunaozai 4 * @date 2018-06-20 5 */ 6 public class SysGrantedAuthority extends BaseModel implements GrantedAuthority { 7 8 private static final long serialVersionUID = 5698641074914331015L; 9 10 /** 11 * 權限 12 */ 13 private String authority; 14 15 /** 16 * 權限 17 * @return authority 18 */ 19 public String getAuthority() { 20 return authority; 21 } 22 23 /** 24 * 權限 25 * @param authority 權限 26 */ 27 public void setAuthority(String authority) { 28 this.authority = authority; 29 } 30 31 }
(7) SysUserAuthentication.java
1 /** 2 * 認證用戶主體模型 3 * @author wunaozai 4 * @date 2018-06-19 5 */ 6 public class SysUserAuthentication extends BaseModel implements UserDetails { 7 8 private static final long serialVersionUID = 2678080792987564753L; 9 10 /** 11 * ID號 12 */ 13 private String uuid; 14 /** 15 * 用戶名 16 */ 17 private String username; 18 /** 19 * 密碼 20 */ 21 private String password; 22 /** 23 * 帳戶生效 24 */ 25 private boolean accountNonExpired; 26 /** 27 * 帳戶鎖定 28 */ 29 private boolean accountNonLocked; 30 /** 31 * 憑證生效 32 */ 33 private boolean credentialsNonExpired; 34 /** 35 * 激活狀態 36 */ 37 private boolean enabled; 38 /** 39 * 權限列表 40 */ 41 private Collection<GrantedAuthority> authorities; 42 /** 43 * ID號 44 * @return uuid 45 */ 46 public String getUuid() { 47 return uuid; 48 } 49 50 /** 51 * ID號 52 * @param uuid ID號 53 */ 54 public void setUuid(String uuid) { 55 this.uuid = uuid; 56 } 57 58 /** 59 * 用戶名 60 * @return username 61 */ 62 public String getUsername() { 63 return username; 64 } 65 66 /** 67 * 用戶名 68 * @param username 用戶名 69 */ 70 public void setUsername(String username) { 71 this.username = username; 72 } 73 74 /** 75 * 密碼 76 * @return password 77 */ 78 public String getPassword() { 79 return password; 80 } 81 82 /** 83 * 密碼 84 * @param password 密碼 85 */ 86 public void setPassword(String password) { 87 this.password = password; 88 } 89 90 /** 91 * 帳戶生效 92 * @return accountNonExpired 93 */ 94 public boolean isAccountNonExpired() { 95 return accountNonExpired; 96 } 97 98 /** 99 * 帳戶生效 100 * @param accountNonExpired 帳戶生效 101 */ 102 public void setAccountNonExpired(boolean accountNonExpired) { 103 this.accountNonExpired = accountNonExpired; 104 } 105 106 /** 107 * 帳戶鎖定 108 * @return accountNonLocked 109 */ 110 public boolean isAccountNonLocked() { 111 return accountNonLocked; 112 } 113 114 /** 115 * 帳戶鎖定 116 * @param accountNonLocked 帳戶鎖定 117 */ 118 public void setAccountNonLocked(boolean accountNonLocked) { 119 this.accountNonLocked = accountNonLocked; 120 } 121 122 /** 123 * 憑證生效 124 * @return credentialsNonExpired 125 */ 126 public boolean isCredentialsNonExpired() { 127 return credentialsNonExpired; 128 } 129 130 /** 131 * 憑證生效 132 * @param credentialsNonExpired 憑證生效 133 */ 134 public void setCredentialsNonExpired(boolean credentialsNonExpired) { 135 this.credentialsNonExpired = credentialsNonExpired; 136 } 137 138 /** 139 * 激活狀態 140 * @return enabled 141 */ 142 public boolean isEnabled() { 143 return enabled; 144 } 145 146 /** 147 * 激活狀態 148 * @param enabled 激活狀態 149 */ 150 public void setEnabled(boolean enabled) { 151 this.enabled = enabled; 152 } 153 154 /** 155 * 權限列表 156 * @return authorities 157 */ 158 public Collection<GrantedAuthority> getAuthorities() { 159 return authorities; 160 } 161 162 /** 163 * 權限列表 164 * @param authorities 權限列表 165 */ 166 public void setAuthorities(Collection<GrantedAuthority> authorities) { 167 this.authorities = authorities; 168 } 169 170 }
3.PostMan工具接口測試
(0) /oauth/token 登陸
這個若是配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的會走ClientCredentialsTokenEndpointFilter來保護
若是沒有支持allowFormAuthenticationForClients或者有支持可是url中沒有client_id和client_secret的,走basic認證保護
(1) /oauth/token client_credentials模式
如代碼所示,增長了一個client/123456 的Client帳戶,裏面有client_credentials受權模式
經過postman請求以下
獲取到access_token後,使用該token請求受保護的資源/order/demo
若是是錯誤的access_token的那麼會提示invalid_token
其實像咱們這種小公司,小項目,基本上用這個也就能夠了,本身的賬號密碼,而後接入第三方微信、QQ之類的。哈哈。
(2) /oauth/token password模式
這種方式比上一種方式更適合咱們公司使用,由於咱們公司對外提供接入方式,基本是提供給咱們的代理商,而咱們更但願賬號和服務都由咱們提供,基本目前幾年內不會提供給代理商第三方登陸,也沒有必要。因此這裏的賬號密碼都是由咱們服務器統一管理。
(3) /oauth/token code 模式
/oauth/authorize
這個比較複雜。我就一步一步的說明。
首先要經過/oauth/token進行登陸,可使用以上(0)(2)方式登陸,注意登陸是scope的填寫。登陸成功後,獲得access_token.而後請求/oauth/authorize地址,注意參數redirect_uri是要跳轉到的第三方地址上。
通常經過GET方式訪問,若是合法的話(合法,判斷access_token和對應的scope)那麼瀏覽器會跳轉到redirect_uri指定的地址。
訪問成功後,會返回一個code值。第三方廠商就能夠根據這個code去獲取用戶的access_token而後訪問受限資源。
一個code只能使用一次,若是屢次使用那麼會報錯
1 { 2 "error": "invalid_grant", 3 "error_description": "Invalid authorization code: 55ffrh" 4 }
注意這裏的redirect_uri根據服務器BaseClientDetailService中配置的uri是一致的,不然不經過。
這種方式是OAuth最好的一種方式,只是基於公司,項目的實際考慮,這種方式,比較繁瑣,目前是不會用到的。
剛纔想了一下,好像第三方獲取到的access_token就是用戶登陸後的access_token,以爲不對,想了想,應該是用戶要經過scope對權限進行限制。而這裏的scope會對應到資源權限部分。
(4) implicit模式 略,基本參考標準OAuth2.0就能夠啦
(5) check_token 檢查token是否合法
(6) refresh_token 刷新token
調用時access_token,refresh_token均未過時 access_token會變,並且expires延長,refresh_token根據設定的過時時間,沒有失效則不變 {"access_token":"eb45f1d4-54a5-4e23-bf12-31d8d91a902f","token_type":"bearer","refresh_token":"efa96270-18a1-432c-b9e6-77725c0dabea","expires_in":1199,"scope":"all"} 調用時access_token過時,refresh_token未過時 access_token會變,並且expires延長,refresh_token根據設定的過時時間,沒有失效則不變 {"access_token":"a78999d6-614a-45fe-be58-d5e0b6451bdb","token_type":"bearer","refresh_token":"bb2a0165-769d-43b0-a9a5-1331012ede1f","expires_in":119,"scope":"all"} 調用時refresh_token過時 {"error":"invalid_token","error_description":"Invalid refresh token (expired): 95844d87-f06e-4a4e-b76c-f16c5329e287"}
關於OAuth裏面的知識還有不少細節沒有理解透,隨着項目的深刻,慢慢了解吧。
-----------------2019-06-11 更新-------------------------
評論區:
問:請教一下根據用戶角色不一樣訪問不一樣請求,這個怎麼搞呢?
答:在 BaseUserDetailService.java 裏面 第2四、28行,表示對當前登陸的帳戶增長一個角色,角色名稱「admin_role」
1 List<GrantedAuthority> list = AuthorityUtils.createAuthorityList("admin_role"); 2 user.setAuthorities(list);
方式1:而後針對URL請求,設置對應的能夠訪問的權限,在 ResourceServerConfiguration.java 第31行
1 .antMatchers("/order/**").hasAuthority("admin_role");//配置訪問控制,必須具備admin_role權限才能夠訪問資源
方式2:上面這種經過配置的方式,有時不是很靈活,通常我是經過註解方式來設置URL請求所須要的權限,下面這個代碼就表示在這整個控制器內的全部請求都是須要「admin_role」權限。
1 @RestController 2 @RequestMapping(value="/order/demo") 3 @PreAuthorize("hasAnyAuthority('admin_role')") 4 public class CustBomController { 5 }
@PreAuthorize這個註解,除了類註解,還能夠對方法體進行註解,註解還能夠經過 and or 進行多個角色權限進行控制。具體你查詢網上資料。
參考資料:
https://github.com/lexburner/oauth2-demo
http://blog.didispace.com/spring-security-oauth2-xjf-1/
https://www.cnblogs.com/cjsblog/p/9152455.html
https://segmentfault.com/a/1190000014371789 (多種認證方式)