Spring Boot 2.0 利用 Spring Security 实现简单的OAuth2.0认证方式2

0.前言

  经过前面一小节已经基本配置好了基于SpringBoot+SpringSecurity+OAuth2.0的环境。这一小节主要对一些写固定InMemory的User和Client进行扩展。实现动态查询用户,但为了演示方便,这里没有查询数据库。仅做Demo演示,最最关键的是,作为我个人笔记。其实代码里面有些注释,可能只有我知道为什么,有些是Debug调试时的一些测试代码。还是建议,读者自己跑一遍会比较好,能跟深入的理解OAuth2.0协议。我也是参考网上很多博客,然后慢慢测试和理解的。
  参考的每个人的博客,都写得很好很仔细,但是有些关键点,还是要自己写个Demo出来才会更好理解。
  结合数据库的,期待下一篇博客

1.目录结构

  
  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 资源服务器配置

2.代码解析

(1) SecurityConfiguration.java

 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.java

 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 里面 第24、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 (多种认证方式)

 

posted @ 2018-06-21 19:52  无脑仔的小明  阅读(57535)  评论(11编辑  收藏  举报