OAuth2的相关定义、原理、4种授权模式、认证流程请参见官网或大佬的相关博文。
oauth2目前出现好多版本依赖,如security中有oauth2、springcloud security中也有oauth2,多种依赖是由其历史原因产生的,在此根据最新版本去学习实践就好,毕竟发布的最新版就oauth2的发展趋势,最后终归一统。
此处引用springcloud中的oauth2依赖进行温习,实践其密码模式,搭建oauth2统一认证中心,应用于分布式架构或微服务架构中,适应多系统需要统一的认证授权需求。
密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器申请令牌(token)。这需要用户对客户端高度信任,例如客户端应用和服务提供商就是同一家公司,我们自己做前后端分离登录就可以采用这种模式。
登录认证分为有状态登录和无状态登录:
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如 Tomcat 中的 Session。例如登录:用户登录后,我们把用户的信息保存在服务端 session 中,并且给用户一个 cookie 值,记录对应的 session,然后下次请求,用户携带 cookie 值来(这一步有浏览器自动完成),我们就能识别到对应 session,从而找到用户的信息。
无状态服务,微服务集群中的每个服务,对外提供的都使用 RESTful 风格的接口。而 RESTful 风格的一个最重要的规范就是:服务的无状态性,即:服务端不保存任何客户端请求者信息;客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份;
此处选择有状态登录,访问授权服务(统一认证中心)获取token,携带token访问资源服务,资源服务远程调用授权服务(统一认证中心)进行鉴权。授权服务(统一认证中心)进行验证有两方面的检验,一方面是校验客户端,另一方面则是校验用户;客户端校验scope,用户校验role,授权码模式就应用到客户端校验scope的选项了。
无状态登录的一种典型代表:JWT。
JWT,全称是 Json Web Token , 是一种 JSON 风格的轻量级的授权和身份认证规范,可实现无状态、分布式的 Web 应用授权;但JWT方式存在续签、注销问题。
有状态登录中授权服务派发了 access_token 之后,客户端拿着 access_token 去请求资源服务,资源服务要去校验 access_token 的真伪,所以我们在资源服务器上配置了 RemoteTokenServices,让资源服务做远程校验,在高并发场景下请求授权服务又不太方便。
单点登录是分布式系统中非常常见的需求,分布式系统由多个不同的子系统组成,而我们在使用系统的时候,只需要登录一次即可,这样其他系统都认为用户已经登录了,不用再去登录。
oauth2+jwt的无状态登录天然满足单点登录的场景;其实oauth2+redis实现共享session的前后端分离模式也满足单点登录的场景;
统一认证中心即将认证授权独立出来,做出单独的一个服务,作为分布式系统的统一认证中心。
此处搭建的统一认证中心即为单独的一个服务,包含授权服务和资源服务(用户),其他子系统也作为资源服务,即一个统一认证中心,多个资源服务
搭建统一认证中心实例
1依赖
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.2.4.RELEASE
com
oauth2
0.0.1-SNAPSHOT
oauth2
Demo project for Spring Boot
1.8
Hoxton.RELEASE
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2
org.springframework.cloud
spring-cloud-starter-oauth2
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.2.0
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
mysql
mysql-connector-java
runtime
com.alibaba
druid-spring-boot-starter
1.1.10
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.security
spring-security-test
test
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
org.projectlombok
lombok
src/main/java
**/*.xml
**/*.properties
false
src/main/resources
**/*.xml
**/*.properties
**/*.yml
false
2application.properties
spring.application.name=oauth2
server.port=
#使用IP注册
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
eureka.client.service-url.defaultZone=http://localhost:9001/eureka/
#mysql配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/oauth2?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=
spring.datasource.password=
3授权服务配置
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private RedisConnectionFactory redisConnectionFactory;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private CustomClientDetailsService customClientDetailsService;
@Bean
public TokenStore tokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//clients.withClientDetails(customClientDetailsService);
// 配置两个客户端,一个用于client认证,一个用于password认证
clients.inMemory()
.withClient("client1")
.secret(passwordEncoder.encode("1"))
.authorizedGrantTypes("client_credentials", "refresh_token")
.resourceIds("resource1")
.scopes("all")
.authorities("oauth2")
.and()
.withClient("client2")
.secret(passwordEncoder.encode("1"))
.authorizedGrantTypes("password", "refresh_token")
.resourceIds("resource2")
.scopes("all")
.authorities("oauth2");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager);
//配置TokenService参数
DefaultTokenServices tokenServices = new DefaultTokenServices();
//token持久化容器
tokenServices.setTokenStore(endpoints.getTokenStore());
//是否支持refresh_token,默认false
tokenServices.setSupportRefreshToken(true);
//客户端信息
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
//自定义token生成
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
//access_token 的有效时长 (秒), 默认 12 小时;1小时
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(1));
//refresh_token 的有效时长 (秒), 默认 30 天;1小时
tokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(1));
//是否复用refresh_token,默认为true(如果为false,则每次请求刷新都会删除旧的refresh_token,创建新的refresh_token)
tokenServices.setReuseRefreshToken(false);
//token相关服务
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
}
4security配置
@Order(1)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private CustomUserDetailsService customUserDetailsService;
@Resource
private CustomAuthenticationProvider customAuthenticationProvider;
@Bean
@Override
protected UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(User.withUsername("admin").password(passwordEncoder().encode("1")).authorities("USER").build());
userDetailsManager.createUser(User.withUsername("user").password(passwordEncoder().encode("1")).authorities("USER").build());
return userDetailsManager;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//自定义用户userDetailService、加密
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
auth.authenticationProvider(customAuthenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().disable();
http
.requestMatchers().antMatchers("/oauth/**")
//拦截上面匹配后的url,需要认证后访问
.and()
.authorizeRequests().antMatchers("/oauth/**").authenticated();
http
.sessionManagement()
.invalidSessionUrl("/login")
.maximumSessions(1)
.expiredUrl("/login");
}
}
5资源服务配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("resource2").stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/oauth2/user", "/user/**").authenticated();
}
}
6资源服务进行验证的接口
/**
* 提供资源服务进行认证校验(必须)
*/
@RequestMapping("/user")
public Principal user(Principal user) {
return user;
}
测试实例
搭建资源服务1,资源服务配置
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("resource2").stateless(true);
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
tokenService.setClientId("client2");
tokenService.setClientSecret("1");
resources.tokenServices(tokenService);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.cors().disable();
http
.authorizeRequests()
.antMatchers("/resource1/**").authenticated();
}
}
或搭建资源服务2,资源服务配置
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("resource2").stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.cors().disable();
http
.authorizeRequests()
.antMatchers("/resource2/**").authenticated();
}
}
application.properties
security.oauth2.client.client-id=client2
security.oauth2.client.client-secret=1
security.oauth2.client.access-token-uri=http://localhost:9002/oauth/token
security.oauth2.client.user-authorization-uri=http://localhost:9002/oauth/authorize
#prefer-token-info默认值为true,既优先使用token-info-uri校验token认证信息
security.oauth2.resource.token-info-uri=http://localhost:9002/oauth/check_token
#进行令牌校验,访问认证服务器Controller获取Principal,解析令牌
security.oauth2.resource.user-info-uri=http://localhost:9002/oauth2/user
#或进行令牌校验,访问认证服务器获取公钥,解析令牌
#security.oauth2.resource.jwt.key-uri=http://localhost:9002/oauth/token_key
#prefer-token-info设置为false,或不配置token-info-uri则会使用user-info-uri
security.oauth2.resource.prefer-token-info=false