网关整合 OAuth2.0 有两种思路,一种是授权服务器生成令牌, 所有请求统一在网关层验证,判断
权限等操作;另一种是由各资源服务处理,网关只做请求转发。 比较常用的是第一种,把API网
关作为OAuth2.0的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当前登录用户信
息给微服务,这样下游微服务就不需要关心令牌格式解析以及OAuth2.0相关机制了。
网关在认证授权体系里主要负责两件事:
(1)作为OAuth2.0的资源服务器角色,实现接入方权限拦截。
(2)令牌解析并转发当前登录用户信息(明文token)给微服务
微服务拿到明文token(明文token中包含登录用户的身份和权限信息)后也需要做两件事:
(1)用户授权拦截(看当前用户是否有权访问该资源)
(2)将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)
(作用:获取token 验证token)
授权配置
(密码模式)
采用redis存token
密钥和appId存在数据库
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`resource_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_secret` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`scope` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorized_grant_types` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`web_server_redirect_uri` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorities` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`access_token_validity` int(0) NULL DEFAULT NULL,
`refresh_token_validity` int(0) NULL DEFAULT NULL,
`additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`autoapprove` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
INSERT INTO `oauth_client_details` VALUES ('appId', 'mayikt_resource', '$2a$10$fBM0guMoKWwZye6u7OAeWuguHL.ElffCe6KQfZsFX44JdQ4gsEDEa', 'all', 'authorization_code,password,client_credentials,refresh_token', 'http://www.mayikt.com/callback', NULL, NULL, NULL, NULL, NULL);
授权配置
/**
* 认证授权Server端
*/
@Component
@EnableAuthorizationServer
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManagerBean;
@Autowired
private UserService userService;
@Autowired
private TokenStore tokenStore;
@Autowired
private DataSource dataSource;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置
.reuseRefreshTokens(false) refresh_token是否重复使用
.tokenStore(tokenStore)
.userDetailsService(userService) //refresh_token是否重复使用
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); //支持GET,POST请求
}
//token http://localhost:8884/oauth/token?username=admin&password=123456&grant_type=password&client_id=appId&client_secret=123456&scope=all
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//允许表单提交
security.allowFormAuthenticationForClients()
.checkTokenAccess("permitAll()");
}
/**
* appid mayikt secret= 123456
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
/**
* 密码模式获取 token http://localhost:8884/oauth/token?username=admin&password=123456&grant_type=password&client_id=appId&client_secret=123456&scope=all
* 刷新密码 http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=appId&client_secret=123456&refresh_token=1b46f93f-af95-4ce6-afae-618eca676ebc
*/
//内存模式
// clients.inMemory()
// // appid 表里取 这里写死
// .withClient("appId")
// // 密钥 表里取 这里写死
// .secret(passwordEncoder.encode("123456"))
// // 授权码
// .authorizedGrantTypes("authorization_code","password","client_credentials","refresh_token")
// // 作用域
// .scopes("all")
// // 资源的id 表里取 这里写死
// .resourceIds("mayikt_resource")
// // 回调地址 表里取 这里写死
// .redirectUris("http://www.mayikt.com/callback");
//数据库模式
clients.withClientDetails(clientDetails());
}
@Bean
public ClientDetailsService clientDetails() {
//读取oauth_client_details表
return new JdbcClientDetailsService(dataSource);
}
}
获取token和刷新token url
获取 token http://localhost:8884/oauth/token?username=admin&password=123456&grant_type=password&client_id=appId&client_secret=123456&scope=all
刷新密码 http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=appId&client_secret=123456&refresh_token=1b46f93f-af95-4ce6-afae-618eca676ebc
security配置
@Component
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().permitAll()
.and().authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.anyRequest().authenticated()
.and().logout().permitAll()
.and().csrf().disable();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
com.huawei.entity.User user = userMapper.selectByName(name);
System.out.println(user+"=======user==========");
return new User(user.getUsername(), user.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRole()));
}
}
redis配置
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore tokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
}
yml配置
server:
port: 8884
spring:
application:
name: my-auth
datasource:
username: root
password: 123456
url: jdbc:mysql://192.168.174.10:3306/oauth2-sso?serverTimezone=GMT%2B8
driver-class-name: com.mysql.jdbc.Driver
redis:
database: 0
host: 127.0.0.1
cloud:
nacos:
discovery:
server-addr: 192.168.174.10:8848
mybatis:
mapper-locations: classpath:mapping/*.xml
type-aliases-package: com.huawei.dao
configuration: #开启sql打印
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
依赖
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-bootartifactId>
<version>2.1.0version>
<type>pomtype>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>1.1.1version>
dependency>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatis-springartifactId>
<version>2.0.0version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.11version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.0.29version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.4version>
<scope>providedscope>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.8.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.8.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-staticdocsartifactId>
<version>2.5.0version>
dependency>
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelperartifactId>
<version>5.0.0version>
dependency>
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelper-spring-boot-autoconfigureartifactId>
<version>1.2.3version>
dependency>
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelper-spring-boot-starterartifactId>
<version>1.2.3version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.8.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.8.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-staticdocsartifactId>
<version>2.5.0version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.security.oauthgroupId>
<artifactId>spring-security-oauth2artifactId>
<version>2.3.3.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
dependencies>
授权认证和路由转发
redis配置
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
//Json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//string序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
@Configuration
public class RibbonConfig {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
过滤认证
/**
* token认证过滤器
*/
@Component
@Order(0) //数值越小优先级越高
public class AuthenticationFilter implements GlobalFilter, InitializingBean {
private static Set<String> noFilterUrl = new LinkedHashSet<>();
@Autowired
private RestTemplate restTemplate;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 过滤前执行这个方法
*
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
// 不拦截认证的请求
noFilterUrl.add("/oauth/token");
noFilterUrl.add("/oauth/checkToken");
noFilterUrl.add("/user/index");
noFilterUrl.add("/user/login");
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String requestPath = request.getURI().getPath();
//不需要认证的url
if (isContinsUrl(requestPath)) {
return chain.filter(exchange);
}
//String token = exchange.getRequest().getQueryParams().getFirst("token");
//获取请求头
String authHeader = request.getHeaders().getFirst("Authorization");
//请求头为空
if (StringUtils.isEmpty(authHeader)) {
throw new RuntimeException("请求头为空");
}
//校验token
TokenInfo tokenInfo = null;
try {
tokenInfo = getTokenInfo(authHeader);
} catch (Exception e) {
throw new RuntimeException("token校验报错");
}
//将token信息带过去
exchange.getAttributes().put("tokenInfo", tokenInfo);
return chain.filter(exchange);
}
private boolean isContinsUrl(String reqPath) {
for (String skipPath : noFilterUrl) {
if (reqPath.contains(skipPath)) {
return true;
}
}
return false;
}
/**
* 校验并获取token
*
* @return
*/
public TokenInfo getTokenInfo(String authHeader) {
// 获取token的值
String token = StringUtils.substringAfter(authHeader, "bearer ");
TokenInfo tokenInfo = null;
//校验token
String tokenUrl = "http://my-auth/oauth/checkToken?token=" + token;
tokenInfo = restTemplate.getForObject(tokenUrl, TokenInfo.class);
//校验成功后将信息存入redis
String username = tokenInfo.getUser_name();
redisTemplate.opsForValue().setIfAbsent(username, JSON.toJSONString(tokenInfo),30L, TimeUnit.MINUTES);
System.out.println("==========redisTemplate===========" + redisTemplate.opsForValue().get(username));
return tokenInfo;
}
}
@Component
@Order(1)
public class AuthorizationFilter implements GlobalFilter, InitializingBean {
private static Set<String> noFilterUrl = new LinkedHashSet<>();
@Autowired
private RoleMapper roleMapper;
@Override
public void afterPropertiesSet() throws Exception {
// 不拦截认证的请求
// 不拦截认证的请求
noFilterUrl.add("/oauth/token");
noFilterUrl.add("/oauth/checkToken");
noFilterUrl.add("/user/index");
noFilterUrl.add("/user/login");
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestPath = exchange.getRequest().getURI().getPath();
//不需要认证的url
if (shouldSkip(requestPath)) {
return chain.filter(exchange);
}
//从上个过滤器获取信息tokenInfo
TokenInfo tokenInfo = exchange.getAttribute("tokenInfo");
if (!tokenInfo.isActive()) {
throw new RuntimeException("token过期");
}
hasPremisson(tokenInfo, requestPath);
return chain.filter(exchange);
}
private boolean shouldSkip(String reqPath) {
for (String skipPath : noFilterUrl) {
if (reqPath.contains(skipPath)) {
return true;
}
}
return false;
}
/**
* 登录用户的权限集合判断
*
* @param tokenInfo
* @param currentUrl
* @return
*/
private void hasPremisson(TokenInfo tokenInfo, String currentUrl) {
boolean hasPremisson = false;
List<String> premessionList = Arrays.asList(tokenInfo.getAuthorities());
//获取登陆用户角色权限有哪些url
Set<String> urlList = new LinkedHashSet<>();
for (String premession : premessionList) {
List<UserRole> userRoles = roleMapper.selectUserRoleByenname(premession);
userRoles.stream().forEach(item -> {
urlList.add(item.getUrl());
});
}
//判断角色权限url 是否包含当前url
for (String u : urlList) {
if (u.equals(currentUrl)) {
hasPremisson = true;
}
}
if (!hasPremisson) {
throw new RuntimeException("没有权限");
}
}
}
yml
server:
port: 9999
spring:
application:
name: my-gataway
cloud:
nacos:
discovery:
server-addr: 192.168.174.10:8848
gateway:
discovery:
locator:
lower-case-service-id: true #微服务名称小写
enabled: true # 打开后只要注册过的服务可以通过服务名转发(
#路由
routes:
- id: routes_1
uri: lb://my-login
predicates:
- Path=/user/**
- id: routes_2
uri: lb://my-producter-servcer
predicates:
- Path=/product/**
datasource:
username: root
password: 123456
url: jdbc:mysql://192.168.174.10:3306/oauth2-sso?serverTimezone=GMT%2B8
driver-class-name: com.mysql.jdbc.Driver
redis:
database: 0
host: 127.0.0.1
port: 6379
mybatis:
mapper-locations: classpath:mapping/*.xml
type-aliases-package: com.huawei.dao
configuration: #开启sql打印
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
源码地址:https://gitee.com/zhu_can_admin/spring-security-oauth2-gateway.git