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
|
1 2 3 4 5 6 7 8 9 10 |
|
关于OAuth里面的知识还有很多细节没有理解透,随着项目的深入,慢慢了解吧。
参考资料:
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 (多种认证方式)
转自:https://www.cnblogs.com/wunaozai/p/9205550.html