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

时间:2021-05-10 17:19:10

0.前言

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

1.目录结构

  Spring Boot 2.0 利用 Spring Security 实现简单的OAuth2.0认证方式2
  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

 /**
* Spring-Security 配置<br>
* 具体参考: 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 (多种认证方式)
* @author wunaozai
* @date 2018-05-28
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //启用方法级的权限认证
public class SecurityConfiguration extends WebSecurityConfigurerAdapter { //通过自定义userDetailsService 来实现查询数据库,手机,二维码等多种验证方式
@Bean
@Override
protected UserDetailsService userDetailsService(){
//采用一个自定义的实现UserDetailsService接口的类
return new BaseUserDetailService();
/*
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String finalPassword = "{bcrypt}"+bCryptPasswordEncoder.encode("123456");
manager.createUser(User.withUsername("user_1").password(finalPassword).authorities("USER").build());
finalPassword = "{noop}123456";
manager.createUser(User.withUsername("user_2").password(finalPassword).authorities("USER").build());
return manager;
*/
} @Override
protected void configure(HttpSecurity http) throws Exception {
// http.authorizeRequests()
// .antMatchers("/", "/index.html", "/oauth/**").permitAll() //允许访问
// .anyRequest().authenticated() //其他地址的访问需要验证权限
// .and()
// .formLogin()
// .loginPage("/login.html") //登录页
// .failureUrl("/login-error.html").permitAll()
// .and()
// .logout()
// .logoutSuccessUrl("/index.html");
http.authorizeRequests().anyRequest().fullyAuthenticated();
http.formLogin().loginPage("/login").failureUrl("/login?code=").permitAll();
http.logout().permitAll();
http.authorizeRequests().antMatchers("/oauth/authorize").permitAll();
} /**
* 用户验证
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
} /**
* Spring Boot 2 配置,这里要bean 注入
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
} @Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}

(2) AuthorizationServerConfiguration.java

 /**
* OAuth 授权服务器配置
* https://segmentfault.com/a/1190000014371789
* @author wunaozai
* @date 2018-05-29
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { private static final String DEMO_RESOURCE_ID = "order"; @Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisConnectionFactory redisConnectionFactory; @Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//String finalSecret = "{bcrypt}"+new BCryptPasswordEncoder().encode("123456");
//clients.setBuilder(builder);
//这里通过实现 ClientDetailsService接口
clients.withClientDetails(new BaseClientDetailService());
/*
//配置客户端,一个用于password认证一个用于client认证
clients.inMemory()
.withClient("client_1")
.resourceIds(DEMO_RESOURCE_ID)
.authorizedGrantTypes("client_credentials", "refresh_token")
.scopes("select")
.authorities("oauth2")
.secret(finalSecret)
.and()
.withClient("client_2")
.resourceIds(DEMO_RESOURCE_ID)
.authorizedGrantTypes("password", "refresh_token")
.scopes("select")
.authorities("oauth2")
.secret(finalSecret)
.and()
.withClient("client_code")
.resourceIds(DEMO_RESOURCE_ID)
.authorizedGrantTypes("authorization_code", "client_credentials", "refresh_token",
"password", "implicit")
.scopes("all")
//.authorities("oauth2")
.redirectUris("http://www.baidu.com")
.accessTokenValiditySeconds(1200)
.refreshTokenValiditySeconds(50000);
*/
} @Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(new RedisTokenStore(redisConnectionFactory))
.authenticationManager(authenticationManager)
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); //配置TokenService参数
DefaultTokenServices tokenService = new DefaultTokenServices();
tokenService.setTokenStore(endpoints.getTokenStore());
tokenService.setSupportRefreshToken(true);
tokenService.setClientDetailsService(endpoints.getClientDetailsService());
tokenService.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenService.setAccessTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(30)); //30天
   tokenService.setRefreshTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(50)); //50天
tokenService.setReuseRefreshToken(false);
endpoints.tokenServices(tokenService); } @Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
//允许表单认证
//这里增加拦截器到安全认证链中,实现自定义认证,包括图片验证,短信验证,微信小程序,第三方系统,CAS单点登录
//addTokenEndpointAuthenticationFilter(IntegrationAuthenticationFilter())
//IntegrationAuthenticationFilter 采用 @Component 注入
oauthServer.allowFormAuthenticationForClients()
.tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("permitAll()");
} }

(3) ResourceServerConfiguration.java

 /**
* OAuth 资源服务器配置
* @author wunaozai
* @date 2018-05-29
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { private static final String DEMO_RESOURCE_ID = "order"; @Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
} @Override
public void configure(HttpSecurity http) throws Exception {
// Since we want the protected resources to be accessible in the UI as well we need
// session creation to be allowed (it's disabled by default in 2.0.6)
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.requestMatchers().anyRequest()
.and()
.anonymous()
.and()
// .authorizeRequests()
// .antMatchers("/order/**").authenticated();//配置order访问控制,必须认证过后才可以访问
.authorizeRequests()
.antMatchers("/order/**").hasAuthority("admin_role");//配置访问控制,必须具有admin_role权限才可以访问资源
// .antMatchers("/order/**").hasAnyRole("admin");
} }

(4) BaseClientDetailService.java

 /**
* 自定义客户端认证
* @author wunaozai
* @date 2018-06-20
*/
public class BaseClientDetailService implements ClientDetailsService { private static final Logger log = LoggerFactory.getLogger(BaseClientDetailService.class); @Override
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
System.out.println(clientId);
BaseClientDetails client = null;
//这里可以改为查询数据库
if("client".equals(clientId)) {
log.info(clientId);
client = new BaseClientDetails();
client.setClientId(clientId);
client.setClientSecret("{noop}123456");
//client.setResourceIds(Arrays.asList("order"));
client.setAuthorizedGrantTypes(Arrays.asList("authorization_code",
"client_credentials", "refresh_token", "password", "implicit"));
//不同的client可以通过 一个scope 对应 权限集
client.setScope(Arrays.asList("all", "select"));
client.setAuthorities(AuthorityUtils.createAuthorityList("admin_role"));
client.setAccessTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(1)); //1天
client.setRefreshTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(1)); //1天
Set<String> uris = new HashSet<>();
uris.add("http://localhost:8080/login");
client.setRegisteredRedirectUri(uris);
}
if(client == null) {
throw new NoSuchClientException("No client width requested id: " + clientId);
}
return client;
} }

(5) BaseUserDetailService.java

 /**
* 自定义用户认证Service
* @author wunaozai
* @date 2018-06-19
*/
//@Service
public class BaseUserDetailService implements UserDetailsService { private static final Logger log = LoggerFactory.getLogger(BaseUserDetailService.class); @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info(username);
System.out.println(username);
//return new User(username, "{noop}123456", false, false, null);
//User user = null;
SysUserAuthentication user = null;
if("admin".equals(username)) {
IntegrationAuthentication auth = IntegrationAuthenticationContext.get();
//这里可以通过auth 获取 user 值
//然后根据当前登录方式type 然后创建一个sysuserauthentication 重新设置 username 和 password
//比如使用手机验证码登录的, username就是手机号 password就是6位的验证码{noop}000000
System.out.println(auth);
List<GrantedAuthority> list = AuthorityUtils.createAuthorityList("admin_role"); //所谓的角色,只是增加ROLE_前缀
user = new SysUserAuthentication();
user.setUsername(username);
user.setPassword("{noop}123456");
user.setAuthorities(list);
user.setAccountNonExpired(true);
user.setAccountNonLocked(true);
user.setCredentialsNonExpired(true);
user.setEnabled(true); //user = new User(username, "{noop}123456", list);
log.info("---------------------------------------------");
log.info(user.toJSONString());
log.info("---------------------------------------------");
//这里会根据user属性抛出锁定,禁用等异常
} return user;//返回UserDetails的实现user不为空,则验证通过
}
}

(6) SysGrantedAuthority.java

 /**
* 授权权限模型
* @author wunaozai
* @date 2018-06-20
*/
public class SysGrantedAuthority extends BaseModel implements GrantedAuthority { private static final long serialVersionUID = 5698641074914331015L; /**
* 权限
*/
private String authority; /**
* 权限
* @return authority
*/
public String getAuthority() {
return authority;
} /**
* 权限
* @param authority 权限
*/
public void setAuthority(String authority) {
this.authority = authority;
} }

(7) SysUserAuthentication.java

 /**
* 认证用户主体模型
* @author wunaozai
* @date 2018-06-19
*/
public class SysUserAuthentication extends BaseModel implements UserDetails { private static final long serialVersionUID = 2678080792987564753L; /**
* ID号
*/
private String uuid;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 账户生效
*/
private boolean accountNonExpired;
/**
* 账户锁定
*/
private boolean accountNonLocked;
/**
* 凭证生效
*/
private boolean credentialsNonExpired;
/**
* 激活状态
*/
private boolean enabled;
/**
* 权限列表
*/
private Collection<GrantedAuthority> authorities;
/**
* ID号
* @return uuid
*/
public String getUuid() {
return uuid;
} /**
* ID号
* @param uuid ID号
*/
public void setUuid(String uuid) {
this.uuid = uuid;
} /**
* 用户名
* @return username
*/
public String getUsername() {
return username;
} /**
* 用户名
* @param username 用户名
*/
public void setUsername(String username) {
this.username = username;
} /**
* 密码
* @return password
*/
public String getPassword() {
return password;
} /**
* 密码
* @param password 密码
*/
public void setPassword(String password) {
this.password = password;
} /**
* 账户生效
* @return accountNonExpired
*/
public boolean isAccountNonExpired() {
return accountNonExpired;
} /**
* 账户生效
* @param accountNonExpired 账户生效
*/
public void setAccountNonExpired(boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
} /**
* 账户锁定
* @return accountNonLocked
*/
public boolean isAccountNonLocked() {
return accountNonLocked;
} /**
* 账户锁定
* @param accountNonLocked 账户锁定
*/
public void setAccountNonLocked(boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
} /**
* 凭证生效
* @return credentialsNonExpired
*/
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
} /**
* 凭证生效
* @param credentialsNonExpired 凭证生效
*/
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
} /**
* 激活状态
* @return enabled
*/
public boolean isEnabled() {
return enabled;
} /**
* 激活状态
* @param enabled 激活状态
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
} /**
* 权限列表
* @return authorities
*/
public Collection<GrantedAuthority> getAuthorities() {
return authorities;
} /**
* 权限列表
* @param authorities 权限列表
*/
public void setAuthorities(Collection<GrantedAuthority> authorities) {
this.authorities = authorities;
} }

3.PostMan工具接口测试

(0) /oauth/token 登录

  这个如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的会走ClientCredentialsTokenEndpointFilter来保护
  如果没有支持allowFormAuthenticationForClients或者有支持但是url中没有client_id和client_secret的,走basic认证保护

(1) /oauth/token client_credentials模式
Spring Boot 2.0 利用 Spring Security 实现简单的OAuth2.0认证方式2
  如代码所示,增加了一个client/123456 的Client账户,里面有client_credentials授权模式
  通过postman请求如下
Spring Boot 2.0 利用 Spring Security 实现简单的OAuth2.0认证方式2
  获取到access_token后,使用该token请求受保护的资源/order/demo
Spring Boot 2.0 利用 Spring Security 实现简单的OAuth2.0认证方式2
  如果是错误的access_token的那么会提示invalid_token
Spring Boot 2.0 利用 Spring Security 实现简单的OAuth2.0认证方式2
  其实像我们这种小公司,小项目,基本上用这个也就可以了,自己的帐号密码,然后接入第三方微信、QQ之类的。哈哈。
(2) /oauth/token password模式
Spring Boot 2.0 利用 Spring Security 实现简单的OAuth2.0认证方式2
  这种方式比上一种方式更适合我们公司使用,因为我们公司对外提供接入方式,基本是提供给我们的代理商,而我们更希望帐号和服务都由我们提供,基本目前几年内不会提供给代理商第三方登录,也没有必要。所以这里的帐号密码都是由我们服务器统一管理。
(3) /oauth/token code 模式
  /oauth/authorize 
  这个比较复杂。我就一步一步的说明。

  首先要通过/oauth/token进行登录,可以使用以上(0)(2)方式登录,注意登录是scope的填写。登录成功后,得到access_token.然后请求/oauth/authorize地址,注意参数redirect_uri是要跳转到的第三方地址上。

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

  一般通过GET方式访问,如果合法的话(合法,判断access_token和对应的scope)那么浏览器会跳转到redirect_uri指定的地址。

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

  访问成功后,会返回一个code值。第三方厂商就可以根据这个code去获取用户的access_token然后访问受限资源。

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

  一个code只能使用一次,如果多次使用那么会报错

 {
"error": "invalid_grant",
"error_description": "Invalid authorization code: 55ffrh"
}

  注意这里的redirect_uri根据服务器BaseClientDetailService中配置的uri是一致的,否则不通过。

  这种方式是OAuth最好的一种方式,只是基于公司,项目的实际考虑,这种方式,比较繁琐,目前是不会用到的。

  刚才想了一下,好像第三方获取到的access_token就是用户登录后的access_token,觉得不对,想了想,应该是用户要通过scope对权限进行限制。而这里的scope会对应到资源权限部分。

(4) implicit模式 略,基本参考标准OAuth2.0就可以啦

(5) check_token 检查token是否合法

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

(6) refresh_token 刷新token

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

调用时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”

 List<GrantedAuthority> list = AuthorityUtils.createAuthorityList("admin_role");
user.setAuthorities(list);

  方式1:然后针对URL请求,设置对应的可以访问的权限,在 ResourceServerConfiguration.java 第31行

 .antMatchers("/order/**").hasAuthority("admin_role");//配置访问控制,必须具有admin_role权限才可以访问资源

  方式2:上面这种通过配置的方式,有时不是很灵活,一般我是通过注解方式来设置URL请求所需要的权限,下面这个代码就表示在这整个控制器内的所有请求都是需要“admin_role”权限。

 @RestController
@RequestMapping(value="/order/demo")
@PreAuthorize("hasAnyAuthority('admin_role')")
public class CustBomController {
}

  @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 (多种认证方式)