Spring Boot 2.0 利用 Spring Security 實現簡單的OAuth2.0認證方式2

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 (多種認證方式)

相關文章
相關標籤/搜索