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 (多种认证方式)
作者:无脑仔的小明 出处:http://www.cnblogs.com/wunaozai/ 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。 如果文中有什么错误,欢迎指出。以免更多的人被误导。有需要沟通的,可以站内私信,文章留言,或者关注“无脑仔的小明”公众号私信我。一定尽力回答。 |