Spring Cloud集成OAuth2
什么是OAuth2
OAuth2 是一个授权框架,或称授权标准,它可以使第三方应用程序或客户端获得对HTTP服务上(例如 Google,GitHub )用户帐户信息的有限访问权限。OAuth 2 通过将用户身份验证委派给托管用户帐户的服务以及授权客户端访问用户帐户进行工作
具体介绍请参考大牛文章:阮一峰的《理解OAuth 2.0》
OAuth2能做什么
OAuth2 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要分享他们的访问许可或他们数据的所有内容
举个栗子:比如我们常用的微信公众号,当我们第一次打开公众号中网页的时候会弹出是否允许授权,当我们点击授权的时候,公众号网站就能获取到我们的头像和昵称等信息。这个过程就是通过OAuth2 来实现的
怎么去使用OAuth2
OAuth2是一套协议,我们可以根据协议来自己编写程序来实现OAuth2的功能,当然我们也可以通过一些框架来实现。由于我们的技术栈是Spring Cloud,那我们就开看看Spring Cloud怎么集成OAuth2。
Spring Cloud集成OAuth2
引入POM依赖
org.springframework.cloud
spring-cloud-starter-security
org.springframework.cloud
spring-cloud-starter-oauth2
编写登录服务
因为授权是用户的授权,所以必须有用户登录才能授权,这里我们使用spring security来实现登录功能
建表语句
CREATE TABLE `ts_users` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_name` varchar(50) NOT NULL COMMENT '用户名',
`user_pwd` varchar(100) NOT NULL COMMENT '用户密码',
PRIMARY KEY (`user_id`) USING BTREE,
UNIQUE KEY `idx_user_name` (`user_name`) USING BTREE,
KEY `idx_user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='用户信息表';
Security配置:SecurityConfig
package com.walle.gatewayserver.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author zhangjiapeng
* @Package com.walle.gatewayserver.config
* @Description: ${todo}
* @date 2019/1/10 16:05
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 用户验证服务
@Autowired
@Qualifier("userDetailServiceImpl")
private UserDetailsService userDetailsService;
// 加密方式 security2.0以后 密码无法明文保存,必须要经过加密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/hello");
}
// 配置拦截规则
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and().formLogin()
.and().csrf().disable()
.httpBasic();
}
}
登录实现:UserDetailServiceImpl
package com.walle.gatewayserver.service.impl;
import com.walle.common.entity.UserInfo;
import com.walle.gatewayserver.dao.UserInfoDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @author zhangjiapeng
* @Package com.walle.gatewayserver.service.impl
* @Description: ${todo}
* @date 2019/1/10 15:54
*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserInfoDao userInfoDao;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查找用户
UserInfo userInfo = userInfoDao.getByUserName(username);
if(userInfo == null){
throw new UsernameNotFoundException("用户不存在");
}
// 权限
GrantedAuthority authority = new SimpleGrantedAuthority("admin");
List authorities = new ArrayList<>(1);
authorities.add(authority);
UserDetails userDetails = new User(userInfo.getUsername(),passwordEncoder.encode(userInfo.getPassword()),authorities);
// 返回用户信息,注意加密
return userDetails;
}
}
暴露接口,这里有两个接口,一个开放给web,一个开放给android
@RestController
@Slf4j
public class UserInfoController {
@GetMapping("/user/web")
public String web(){
return "hello web";
}
@GetMapping("/user/android")
public String android(){
return "hello android";
}
}
启动服务:访问http://localhost:9001/user/web,然后会看到登录界面
输入账号密码后看到
访问http://localhost:9001/user/android
这时候我们的登录用户可以访问到所有的资源,但是我们想让web登录的用户只能看到web,android的用户只能看到android。我们通过OAuth2来实现这个功能
编写授权服务
package com.walle.gatewayserver.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.client.InMemoryClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import java.util.concurrent.TimeUnit;
/**
* @author zhangjiapeng
* @Package com.walle.gatewayserver.config
* @Description: ${todo}
* @date 2019/1/10 16:39
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("userDetailServiceImpl")
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Bean
public InMemoryTokenStore tokenStore(){
return new InMemoryTokenStore();
}
@Bean
public InMemoryClientDetailsService clientDetails() {
return new InMemoryClientDetailsService();
}
// 配置token
@Bean
@Primary
public DefaultTokenServices tokenService(){
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(clientDetails());
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30));
return tokenServices;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore())
.userDetailsService(userDetailsService)
.authenticationManager(authenticationManager)
.tokenServices(tokenService());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
// 设置客户端信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("web")
.scopes("web")
.secret(passwordEncoder.encode("web"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.redirectUris("http://www.baidu.com")
.and().withClient("android")
.scopes("android")
.secret(passwordEncoder.encode("android"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.redirectUris("http://www.baidu.com");
}
}
这里是通过内存模式配置了两个客户端
客户端:web 密码: web scopes: web
客户端:android 密码: web scopes: android
配置资源服务
package com.walle.gatewayserver.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
/**
* @author zhangjiapeng
* @Package com.walle.gatewayserver.config
* @Description: ${todo}
* @date 2019/1/10 16:29
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.requestMatchers().antMatchers("/user/**")
.and()
.authorizeRequests()
.antMatchers("/user/web").access("#oauth2.hasScope('web')")
.antMatchers("/user/android").access("#oauth2.hasScope('android')")
.anyRequest().permitAll();
}
}
这里我们可以看到,我们通过配置资源服务拦截所有的/user/**的请求,然后请求/user/android必须有scope=android
这时候在登录访问下http://localhost:9001/user/android
这时候我们看到报错页面,需要验证才能访问。
获取授权
访问http://localhost:9001/oauth/authorize?client_id=android&response_type=code&redirect_uri=http://www.baidu.com
然后跳转到一个授权页面
是不是跟微信很像,这里说是否授权web,我们选择Approve ,然后页面跳转到了
https://www.baidu.com/?code=XOCtGr
我们拿到了一个code,然后我们通过code去获取access_token
POST访问http://localhost:9001/oauth/token?clientId=android&grant_type=authorization_code&code=A9bCN5&redirect_uri=http://www.baidu.com
这样我们就获得了access_token
这时候我们访问http://localhost:9001/user/android?access_token=59cf521c-026f-4df1-974e-3e4bfc42e432
看到
OK,android可以访问了,我们试试web能不能访问呢,访问http://localhost:9001/user/web?access_token=59cf521c-026f-4df1-974e-3e4bfc42e432
提示我们不合适的scope,这样我们就实现了不同客户端访问不同资源的权限控制
完!