SpringCloud微服务实战——搭建企业级开发框架(二十三):Gateway+OAuth2+JWT微服务统一认证授权【一】
【摘要】 OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌),使得第三方应用可以使用该token(令牌)在限定时间、限定范围访问指定资源。 OAuth2中使用token验证用户登录合法性,但token最大的问题是不携带用户信息,资源服务器无法在本地进行验证,每次对于资源的访问,资源服务器都需要向认证服务器发起请...
OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌),使得第三方应用可以使用该token(令牌)在限定时间、限定范围访问指定资源。
OAuth2中使用token验证用户登录合法性,但token最大的问题是不携带用户信息,资源服务器无法在本地进行验证,每次对于资源的访问,资源服务器都需要向认证服务器发起请求,一是验证token的有效性,二是获取token对应的用户信息。如果有大量的此类请求,无疑处理效率是很低,且认证服务器会变成一个中心节点,这在分布式架构下很影响性能。如果认证服务器颁发的是jwt格式的token,那么资源服务器就可以直接自己验证token的有效性并绑定用户,这无疑大大提升了处理效率且减少了单点隐患。
SpringCloud认证授权解决思路:认证服务负责认证,网关负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。
微服务鉴权功能划分:
- gitegg-oauth:Oauth2用户认证和单点登录
- gitegg-gateway:请求转发和统一鉴权
- gitegg-system: 读取系统配置的RBAC权限配置并存放到缓存
一、鉴权配置
- GitEgg-Platform工程下新建gitegg-platform-oauth2工程,用于统一管理OAuth2版本,及统一配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>GitEgg-Platform</artifactId>
<groupId>com.gitegg.platform</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gitegg-platform-oauth2</artifactId>
<name>${project.artifactId}</name>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>com.gitegg.platform</groupId>
<artifactId>gitegg-platform-swagger</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
- 在gitegg-oauth工程中引入需要的库
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>GitEgg-Cloud</artifactId>
<groupId>com.gitegg.cloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gitegg-oauth</artifactId>
<name>${project.artifactId}</name>
<packaging>jar</packaging>
<dependencies>
<!-- gitegg-platform-boot -->
<dependency>
<groupId>com.gitegg.platform</groupId>
<artifactId>gitegg-platform-boot</artifactId>
<version>${gitegg.project.version}</version>
</dependency>
<!-- gitegg-platform-cloud -->
<dependency>
<groupId>com.gitegg.platform</groupId>
<artifactId>gitegg-platform-cloud</artifactId>
<version>${gitegg.project.version}</version>
</dependency>
<!-- gitegg-platform-oauth2 -->
<dependency>
<groupId>com.gitegg.platform</groupId>
<artifactId>gitegg-platform-oauth2</artifactId>
<version>${gitegg.project.version}</version>
</dependency>
<!-- gitegg数据库驱动及连接池 -->
<dependency>
<groupId>com.gitegg.platform</groupId>
<artifactId>gitegg-platform-db</artifactId>
</dependency>
<!-- gitegg mybatis-plus -->
<dependency>
<groupId>com.gitegg.platform</groupId>
<artifactId>gitegg-platform-mybatis</artifactId>
</dependency>
<!-- 验证码 -->
<dependency>
<groupId>com.gitegg.platform</groupId>
<artifactId>gitegg-platform-captcha</artifactId>
</dependency>
<!-- gitegg-service-system 的fegin公共调用方法 -->
<dependency>
<groupId>com.gitegg.cloud</groupId>
<artifactId>gitegg-service-system-api</artifactId>
<version>${gitegg.project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
</project>
- JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。首先我们使用keytool生成RSA证书gitegg.jks,复制到gitegg-oauth工程的resource目录下,CMD命令行进入到JDK安装目录的bin目录下, 使用keytool命令生成gitegg.jks证书
keytool -genkey -alias gitegg -keyalg RSA -keystore gitegg.jks
- 新建GitEggUserDetailsServiceImpl.java实现SpringSecurity获取用户信息接口,用于SpringSecurity鉴权时获取用户信息
package com.gitegg.oauth.service;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.common.exceptions.UserDeniedAuthorizationException;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import com.gitegg.oauth.enums.AuthEnum;
import com.gitegg.platform.base.constant.AuthConstant;
import com.gitegg.platform.base.domain.GitEggUser;
import com.gitegg.platform.base.enums.ResultCodeEnum;
import com.gitegg.platform.base.result.Result;
import com.gitegg.service.system.api.feign.IUserFeign;
import cn.hutool.core.bean.BeanUtil;
import lombok.RequiredArgsConstructor;
/**
* 实现SpringSecurity获取用户信息接口
*
* @author gitegg
*/
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class GitEggUserDetailsServiceImpl implements UserDetailsService {
private final IUserFeign userFeign;
private final HttpServletRequest request;
@Override
public GitEggUserDetails loadUserByUsername(String username) {
// 获取登录类型,密码,二维码,验证码
String authLoginType = request.getParameter(AuthConstant.AUTH_TYPE);
// 获取客户端id
String clientId = request.getParameter(AuthConstant.AUTH_CLIENT_ID);
// 远程调用返回数据
Result<Object> result;
// 通过手机号码登录
if (!StringUtils.isEmpty(authLoginType) && AuthEnum.PHONE.code.equals(authLoginType))
{
String phone = request.getParameter(AuthConstant.PHONE_NUMBER);
result = userFeign.queryUserByPhone(phone);
}
// 通过账号密码登录
else if(!StringUtils.isEmpty(authLoginType) && AuthEnum.QR.code.equals(authLoginType))
{
result = userFeign.queryUserByAccount(username);
}
else
{
result = userFeign.queryUserByAccount(username);
}
// 判断返回信息
if (null != result && result.isSuccess()) {
GitEggUser gitEggUser = new GitEggUser();
BeanUtil.copyProperties(result.getData(), gitEggUser, false);
if (gitEggUser == null || gitEggUser.getId() == null) {
throw new UsernameNotFoundException(ResultCodeEnum.INVALID_USERNAME.msg);
}
if (CollectionUtils.isEmpty(gitEggUser.getRoleIdList())) {
throw new UserDeniedAuthorizationException(ResultCodeEnum.INVALID_ROLE.msg);
}
return new GitEggUserDetails(gitEggUser.getId(), gitEggUser.getTenantId(), gitEggUser.getOauthId(),
gitEggUser.getNickname(), gitEggUser.getRealName(), gitEggUser.getOrganizationId(),
gitEggUser.getOrganizationName(),
gitEggUser.getOrganizationIds(), gitEggUser.getOrganizationNames(), gitEggUser.getRoleId(), gitEggUser.getRoleIds(), gitEggUser.getRoleName(), gitEggUser.getRoleNames(),
gitEggUser.getRoleIdList(), gitEggUser.getRoleKeyList(), gitEggUser.getResourceKeyList(),
gitEggUser.getDataPermission(),
gitEggUser.getAvatar(), gitEggUser.getAccount(), gitEggUser.getPassword(), true, true, true, true,
AuthorityUtils.createAuthorityList(gitEggUser.getRoleIdList().toArray(new String[gitEggUser.getRoleIdList().size()])));
} else {
throw new UsernameNotFoundException(result.getMsg());
}
}
}
- 新建AuthorizationServerConfig.java用于认证服务相关配置,正式环境请一定记得修改gitegg.jks配置的密码,这里默认为123456。TokenEnhancer 为登录用户的扩展信息,可以自己定义。
package com.gitegg.oauth.config;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
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.TokenGranter;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import com.anji.captcha.service.CaptchaService;
import com.gitegg.oauth.granter.GitEggTokenGranter;
import com.gitegg.oauth.service.GitEggClientDetailsServiceImpl;
import com.gitegg.oauth.service.GitEggUserDetails;
import com.gitegg.platform.base.constant.AuthConstant;
import com.gitegg.platform.base.constant.TokenConstant;
import com.gitegg.service.system.api.feign.IUserFeign;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
/**
* 认证服务配置
*/
@Configuration
@EnableAuthorizationServer
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final DataSource dataSource;
private final AuthenticationManager authenticationManager;
private final UserDetailsService userDetailsService;
private final IUserFeign userFeign;
private final RedisTemplate redisTemplate;
private final CaptchaService captchaService;
@Value("${captcha.type}")
private String captchaType;
/**
* 客户端信息配置
*/
@Override
@SneakyThrows
public void configure(ClientDetailsServiceConfigurer clients) {
GitEggClientDetailsServiceImpl jdbcClientDetailsService = new GitEggClientDetailsServiceImpl(dataSource);
jdbcClientDetailsService.setFindClientDetailsSql(AuthConstant.FIND_CLIENT_DETAILS_SQL);
jdbcClientDetailsService.setSelectClientDetailsSql(AuthConstant.SELECT_CLIENT_DETAILS_SQL);
clients.withClientDetails(jdbcClientDetailsService);
}
/**
* 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
tokenEnhancers.add(tokenEnhancer());
tokenEnhancers.add(jwtAccessTokenConverter());
tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
// 获取自定义tokenGranter
TokenGranter tokenGranter = GitEggTokenGranter.getTokenGranter(authenticationManager, endpoints, redisTemplate,
userFeign, captchaService, captchaType);
endpoints.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter())
.tokenEnhancer(tokenEnhancerChain)
.userDetailsService(userDetailsService)
.tokenGranter(tokenGranter)
/**
*
* refresh_token有两种使用方式:重复使用(true)、非重复使用(false),默认为true
* 1.重复使用:access_token过期刷新时, refresh token过期时间未改变,仍以初次生成的时间为准
* 2.非重复使用:access_token过期刷新时, refresh_token过期时间延续,在refresh_token有效期内刷新而无需失效再次登录
*/
.reuseRefreshTokens(false);
}
/**
* 允许表单认证
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
/**
* 使用非对称加密算法对token签名
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyPair());
return converter;
}
/**
* 从classpath下的密钥库中获取密钥对(公钥+私钥)
*/
@Bean
public KeyPair keyPair() {
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(
new ClassPathResource("gitegg.jks"), "123456".toCharArray());
KeyPair keyPair = factory.getKeyPair(
"gitegg", "123456".toCharArray());
return keyPair;
}
/**
* JWT内容增强
*/
@Bean
public TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) -> {
Map<String, Object> map = new HashMap<>(2);
GitEggUserDetails user = (GitEggUserDetails) authentication.getUserAuthentication().getPrincipal();
map.put(TokenConstant.TENANT_ID, user.getTenantId());
map.put(TokenConstant.OAUTH_ID, user.getOauthId());
map.put(TokenConstant.USER_ID, user.getId());
map.put(TokenConstant.ORGANIZATION_ID, user.getOrganizationId());
map.put(TokenConstant.ORGANIZATION_NAME, user.getOrganizationName());
map.put(TokenConstant.ORGANIZATION_IDS, user.getOrganizationIds());
map.put(TokenConstant.ORGANIZATION_NAMES, user.getOrganizationNames());
map.put(TokenConstant.ROLE_ID, user.getRoleId());
map.put(TokenConstant.ROLE_NAME, user.getRoleName());
map.put(TokenConstant.ROLE_IDS, user.getRoleIds());
map.put(TokenConstant.ROLE_NAMES, user.getRoleNames());
map.put(TokenConstant.ACCOUNT, user.getAccount());
map.put(TokenConstant.REAL_NAME, user.getRealName());
map.put(TokenConstant.NICK_NAME, user.getNickname());
map.put(TokenConstant.ROLE_ID_LIST, user.getRoleIdList());
map.put(TokenConstant.ROLE_KEY_LIST, user.getRoleKeyList());
//不把权限菜单放到jwt里面,当菜单太多时,会导致jwt长度不可控
// map.put(TokenConstant.RESOURCE_KEY_LIST, user.getResourceKeyList());
map.put(TokenConstant.DATA_PERMISSION, user.getDataPermission());
map.put(TokenConstant.AVATAR, user.getAvatar());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
return accessToken;
};
}
}
- Gateway在认证授权时需要RSA的公钥来验证签名是否合法,所以这里新建GitEggOAuthController的getKey接口用于Gateway获取RSA公钥
@GetMapping("/public_key")
public Map<String, Object> getKey() {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAKey key = new RSAKey.Builder(publicKey).build();
return new JWKSet(key).toJSONObject();
}
- 新建ResourceServerConfig.java资源服务器配置,放开public_key的读取权限
@Override
@SneakyThrows
public void configure(HttpSecurity http) {
http.headers().frameOptions().disable();
http.formLogin()
.and()
.authorizeRequests().requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
.and()
.authorizeRequests()
.antMatchers(
"/oauth/public_key").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
- 在gitegg-service-system新建InitResourceRolesCacheRunner.java实现CommandLineRunner接口,用于系统启动时加载RBAC权限配置信息到缓存
package com.gitegg.service.system.component;
import java.util.*;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import com.gitegg.platform.base.constant.AuthConstant;
import com.gitegg.service.system.entity.Resource;
import com.gitegg.service.system.service.IResourceService;
import cn.hutool.core.collection.CollectionUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 容器启动完成加载资源权限数据到缓存
*/
@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@Component
public class InitResourceRolesCacheRunner implements CommandLineRunner {
private final RedisTemplate redisTemplate;
private final IResourceService resourceService;
/**
* 是否开启租户模式
*/
@Value(("${tenant.enable}"))
private Boolean enable;
@Override
public void run(String... args) {
log.info("InitResourceRolesCacheRunner running");
// 查询系统角色和权限的关系
List<Resource> resourceList = resourceService.queryResourceRoleIds();
// 判断是否开启了租户模式,如果开启了,那么角色权限需要按租户进行分类存储
if (enable) {
Map<Long, List<Resource>> resourceListMap =
resourceList.stream().collect(Collectors.groupingBy(Resource::getTenantId));
resourceListMap.forEach((key, value) -> {
String redisKey = AuthConstant.TENANT_RESOURCE_ROLES_KEY + key;
redisTemplate.delete(redisKey);
addRoleResource(redisKey, value);
System.out.println(redisTemplate.opsForHash().entries(redisKey).size());
});
} else {
redisTemplate.delete(AuthConstant.RESOURCE_ROLES_KEY);
addRoleResource(AuthConstant.RESOURCE_ROLES_KEY, resourceList);
}
}
private void addRoleResource(String key, List<Resource> resourceList) {
Map<String, List<String>> resourceRolesMap = new TreeMap<>();
Optional.ofNullable(resourceList).orElse(new ArrayList<>()).forEach(resource -> {
// roleId -> ROLE_{roleId}
List<String> roles = Optional.ofNullable(resource.getRoleIds()).orElse(new ArrayList<>()).stream()
.map(roleId -> AuthConstant.AUTHORITY_PREFIX + roleId).collect(Collectors.toList());
if (CollectionUtil.isNotEmpty(roles)) {
resourceRolesMap.put(resource.getResourceUrl(), roles);
}
});
redisTemplate.opsForHash().putAll(key, resourceRolesMap);
}
}
- 新建网关服务gitegg-gateway,作为Oauth2的资源服务、客户端服务使用,对访问微服务的请求进行转发、统一校验认证和鉴权操作,引入相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>GitEgg-Cloud</artifactId>
<groupId>com.gitegg.cloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gitegg-gateway</artifactId>
<dependencies>
<dependency>
<groupId>com.gitegg.platform</groupId>
<artifactId>gitegg-platform-base</artifactId>
<version>${gitegg.project.version}</version>
</dependency>
<!-- Nacos 服务注册发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Nacos 分布式配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- OpenFeign 微服务调用解决方案 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.gitegg.platform</groupId>
<artifactId>gitegg-platform-oauth2</artifactId>
<version>${gitegg.project.version}</version>
</dependency>
<!-- gitegg cache自定义扩展 -->
<dependency>
<groupId>com.gitegg.platform</groupId>
<artifactId>gitegg-platform-cache</artifactId>
<version>${gitegg.project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-ui</artifactId>
</dependency>
</dependencies>
</project>
未完,因篇幅限制20000字,剩余内容请看下一章: SpringCloud微服务实战——搭建企业级开发框架(二十三):Gateway+OAuth2+JWT微服务统一认证授权【二】
GitEgg-Cloud是一款基于SpringCloud整合搭建的企业级微服务应用开发框架,开源项目地址:
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)