微服务网关整合 OAuth2.0 思路分析
网关整合 OAuth2.0 有两种思路,一种是授权服务器生成令牌, 所有请求统一在网关层验证,判断权
限等操作;另一种是由各资源服务处理,网关只做请求转发。 比较常用的是第一种,把API网关作
为OAuth2.0的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当前登录用户信息给微
服务,这样下游微服务就不需要关心令牌格式解析以及OAuth2.0相关机制了。
网关在认证授权体系里主要负责两件事:
(1)作为OAuth2.0的资源服务器角色,实现接入方访问权限拦截。
(2)令牌解析并转发当前登录用户信息(明文token)给微服务
微服务拿到明文token(明文token中包含登录用户的身份和权限信息)后也需要做两件事:
(1)用户授权拦截(看当前用户是否有权访问该资源)
(2)将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)
大致流程
授权中心
<!-- spring security oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
数据库 表
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(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`access_token_validity` int(11) NULL DEFAULT NULL,
`refresh_token_validity` int(11) NULL DEFAULT NULL,
`additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`autoapprove` varchar(256) 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;
实现非对称加密
第一步:生成jks 证书文件
我们使用jdk自动的工具生成
命令格式
keytool
-genkeypair 生成密钥对
-alias jwt(别名)
-keypass 123456(别名密码)
-keyalg RSA(生证书的算法名称,RSA是一种非对称加密算法)
-keysize 1024(密钥长度,证书大小)
-validity 365(证书有效期,天单位)
-keystore D:/jwt/jwt.jks(指定生成证书的位置和证书名称)
-storepass 123456(获取keystore信息的密码)
-storetype (指定密钥仓库类型)
使用 “keytool -help” 获取所有可用命令
keytool ‐genkeypair ‐alias jwt ‐keyalg RSA ‐keysize 2048 ‐keystore D:/jwt/jwt.jks
将生成的jwt.jks文件cope到授权服务器的resource目录下
查看公钥信息
keytool ‐list ‐rfc ‐‐keystore jwt.jks | openssl x509 ‐inform pem ‐pubkey
第二步:授权服务中增加jwt的属性配置类
/**
* @vlog: 高于生活,源于生活
* @desc: 类的描述: jwt 证书配置
* @author: smlz
* @version: 1.0
*/
@Data
@ConfigurationProperties(prefix = "my.jwt")
public class JwtCAProperties {
/**
* 证书名称
*/
private String keyPairName;
/**
* 证书别名
*/
private String keyPairAlias;
/**
* 证书私钥
*/
private String keyPairSecret;
/**
* 证书存储密钥
*/
private String keyPairStoreSecret;
}
yml中添加jwt配置
tuling:
jwt:
keyPairName: jwt.jks
keyPairAlias: jwt
keyPairSecret: 123123
keyPairStoreSecret: 123123
第三步:修改AuthServerConfig的配置,支持非对称加密
@Configuration
@EnableAuthorizationServer
@EnableConfigurationProperties(value = JwtCAProperties.class)
public class MyAuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
@Autowired
private MyUserDetailService myUserDetailService;
//修改JwtTokenStoreConfig的配置,支持非对称加密
@Autowired
private JwtCAProperties jwtCAProperties;
/**
* 方法实现说明:我们颁发的token通过jwt存储
* @author:smlz
* @return:
* @exception:
* @date:2020/1/21 21:49
*/
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//jwt的密钥
converter.setKeyPair(keyPair());
return converter;
}
@Bean
public KeyPair keyPair() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource(jwtCAProperties.getKeyPairName()), jwtCAProperties.getKeyPairSecret().toCharArray());
return keyStoreKeyFactory.getKeyPair(jwtCAProperties.getKeyPairAlias(), jwtCAProperties.getKeyPairStoreSecret().toCharArray());
}
//扩展JWT中的存储内容
@Bean
public MyTokenEnhancer myTokenEnhancer() {
return new MyTokenEnhancer();
}
/**
* 方法实现说明:认证服务器能够给哪些 客户端颁发token 我们需要把客户端的配置 存储到
* 数据库中 可以基于内存存储和db存储
* @author:smlz
* @return:
* @exception:
* @date:2020/1/15 20:18
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetails());
}
/**
* 方法实现说明:用于查找我们第三方客户端的组件 主要用于查找 数据库表 oauth_client_details
* @author:smlz
* @return:
* @exception:
* @date:2020/1/15 20:19
*/
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
/**
* 方法实现说明:授权服务器的配置的配置
* @author:smlz
* @return:
* @exception:
* @date:2020/1/15 20:21
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(myTokenEnhancer(),jwtAccessTokenConverter()));
endpoints.tokenStore(tokenStore()) //授权服务器颁发的token 怎么存储的
.tokenEnhancer(tokenEnhancerChain)
.userDetailsService(myUserDetailService) //用户来获取token的时候需要 进行账号密码
.authenticationManager(authenticationManager);
}
/**
* 方法实现说明:授权服务器安全配置
* @author:smlz
* @return:
* @exception:
* @date:2020/1/15 20:23
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//第三方客户端校验token需要带入 clientId 和clientSecret来校验
security .checkTokenAccess("isAuthenticated()")
.tokenKeyAccess("isAuthenticated()");//来获取我们的tokenKey需要带入clientId,clientSecret
security.allowFormAuthenticationForClients();
}
第四步:扩展JWT中的存储内容
MyTokenEnhancer
/**
* @vlog: 高于生活,源于生活
* @desc: 类的描述:jwt自定义增强器(根据自己的业务需求添加非敏感字段)
* @author: yuyang
* @version: 1.0
*/
public class MyTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal();
final Map<String, Object> additionalInfo = new HashMap<>();
final Map<String, Object> retMap = new HashMap<>();
//这里暴露memberId到Jwt的令牌中,后期可以根据自己的业务需要 进行添加字段
additionalInfo.put("memberId",memberDetails.getUmsMember().getId());
additionalInfo.put("nickName",memberDetails.getUmsMember().getNickname());
additionalInfo.put("integration",memberDetails.getUmsMember().getIntegration());
retMap.put("additionalInfo",additionalInfo);
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(retMap);
return accessToken;
}
}
配置SpringSecurity
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailService myUserDetailService;
/**
* 方法实现说明:用于构建用户认证组件,需要传递userDetailsService和密码加密器
* @author:smlz
* @param auth
* @return:
* @exception:
* @date:2019/12/25 14:31
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService).passwordEncoder(passwordEncoder());
}
/**
* 设置前台静态资源不拦截
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**", "/css/**", "/images/**");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 密码模式需要这个bean
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
// oauth2 密码模式需要拿到这个bean
return super.authenticationManagerBean();
}
}
UserDetailService
@Slf4j
@Component
public class MyUserDetailService implements UserDetailsService {
/**
* 方法实现说明:用户登陆
* @author:smlz
* @param userName 用户名密码
* @return: UserDetails
* @exception:
* @date:2020/1/21 21:30
*/
@Autowired
private MemberMapper memberMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
if(StringUtils.isEmpty(userName)) {
log.warn("用户登陆用户名为空:{}",userName);
throw new UsernameNotFoundException("用户名不能为空");
}
Member member = getByUsername(userName);
if(null == member ) {
log.warn("根据用户名没有查询到对应的用户信息:{}",userName);
}
log.info("根据用户名:{}获取用户登陆信息:{}",userName,member );
MemberDetails memberDetails = new MemberDetails(member );
return memberDetails;
}
/**
* 方法实现说明:根据用户名获取用户信息
* @author:smlz
* @param username:用户名
* @return: UmsMember 会员对象
* @exception:
* @date:2020/1/21 21:34
*/
public Member getByUsername(String username) {
MemberExample example = new MemberExample();
example.createCriteria().andUsernameEqualTo(username);
List<Member> memberList = memberMapper.selectByExample(example);
if (!CollectionUtils.isEmpty(memberList)) {
return memberList.get(0);
}
return null;
}
}
UserDetial类
public class MemberDetails implements UserDetails {
private UmsMember umsMember;
public MemberDetails(UmsMember umsMember) {
this.umsMember = umsMember;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//返回当前用户的权限
return Arrays.asList(new SimpleGrantedAuthority("TEST"));
}
@Override
public String getPassword() {
return umsMember.getPassword();
}
@Override
public String getUsername() {
return umsMember.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return umsMember.getStatus()==1;
}
public UmsMember getUmsMember() {
return umsMember;
}
}
接入网关gateway
认证过滤器AuthenticationFilter#filter中需要实现的逻辑
//1.过滤不需要认证的url,比如/oauth/**
//2. 获取token
//从请求头中解析 Authorization value: bearer xxxxxxx
//或者从请求参数中解析 access_token
//3. 校验token
// 拿到token后,通过公钥(需要从授权服务获取公钥)校验
// 校验失败或超时抛出异常
//4. 校验通过后,从token中获取的用户登录信息存储到请求头中
JWT依赖
<!‐‐添加jwt相关的包‐‐>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt‐api</artifactId>
<version>0.10.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt‐impl</artifactId>
<version>0.10.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt‐jackson</artifactId>
<version>0.10.5</version>
<scope>runtime</scope>
</dependency>
编写GateWay的全局过滤器进行权限的校验拦截
@Component
@Slf4j
@EnableConfigurationProperties(value = NotAuthUrlProperties.class)
public class AuthorizationFilter implements GlobalFilter,Ordered,InitializingBean {
@Autowired
private RestTemplate restTemplate;
/**
* 请求各个微服务 不需要用户认证的URL
*/
@Autowired
private NotAuthUrlProperties notAuthUrlProperties;
/**
* jwt的公钥,需要网关启动,远程调用认证中心去获取公钥
*/
private PublicKey publicKey;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String currentUrl = exchange.getRequest().getURI().getPath();
//1:不需要认证的url
if(shouldSkip(currentUrl)) {
//log.info("跳过认证的URL:{}",currentUrl);
return chain.filter(exchange);
}
//log.info("需要认证的URL:{}",currentUrl);
//第一步:解析出我们Authorization的请求头 value为: “bearer XXXXXXXXXXXXXX”
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
//第二步:解析请求,获取token,判断Authorization的请求头是否为空
if(StringUtils.isEmpty(authHeader)) {
log.warn("需要认证的url,请求头为空");
throw new GateWayException(ResultCode.AUTHORIZATION_HEADER_IS_EMPTY);
}
//第三步 校验我们的jwt 若jwt不对或者超时都会抛出异常
Claims claims = JwtUtils.validateJwtToken(authHeader,publicKey);
//第四步 把从jwt中解析出来的 用户登陆信息存储到请求头中
ServerWebExchange webExchange = wrapHeader(exchange,claims);
return chain.filter(webExchange);
}
/**
* 方法实现说明:把我们从jwt解析出来的用户信息存储到请求中
* @author:smlz
* @param serverWebExchange
* @param claims
* @return: ServerWebExchange
* @exception:
* @date:2020/1/22 12:12
*/
private ServerWebExchange wrapHeader(ServerWebExchange serverWebExchange,Claims claims) {
String loginUserInfo = JSON.toJSONString(claims);
//log.info("jwt的用户信息:{}",loginUserInfo);
String memberId = claims.get("additionalInfo",Map.class).get("memberId").toString();
String nickName = claims.get("additionalInfo",Map.class).get("nickName").toString();
//向headers中放文件,记得build
ServerHttpRequest request = serverWebExchange.getRequest().mutate()
.header("username",claims.get("user_name",String.class))
.header("memberId",memberId)
.header("nickName",nickName)
.build();
//将现在的request 变成 change对象
return serverWebExchange.mutate().request(request).build();
}
/**
* 方法实现说明:不需要授权的路径
* @author:smlz
* @param currentUrl 当前请求路径
* @return:
* @exception:
* @date:2019/12/26 13:49
*/
private boolean shouldSkip(String currentUrl) {
//路径匹配器(简介SpringMvc拦截器的匹配器)
//比如/oauth/** 可以匹配/oauth/token /oauth/check_token等
PathMatcher pathMatcher = new AntPathMatcher();
for(String skipPath:notAuthUrlProperties.getShouldSkipUrls()) {
if(pathMatcher.match(skipPath,currentUrl)) {
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
/**
* 方法实现说明:网关服务启动 生成公钥
* @author:smlz
* @return:
* @exception:
* @date:2020/1/22 11:58
*/
@Override
public void afterPropertiesSet() throws Exception {
//初始化公钥
this.publicKey = JwtUtils.genPulicKey(restTemplate);
}
}
设置非拦截路径
/**
* @vlog: 高于生活,源于生活
* @desc: 类的描述:网关跳过认证的配置类
* @author: smlz
* @createDate: 2020/1/22 10:56
* @version: 1.0
*/
@Data
@ConfigurationProperties("my.gateway")
public class NotAuthUrlProperties {
private LinkedHashSet<String> shouldSkipUrls;
}
//application.yml
my:
gateway:
shouldSkipUrls:
- /oauth/**
- /sso/**
- /home/**
- /portal/commentlist/**
- /order/paySuccess/**
- /pms/**
- /static/qrcode/**
校验token逻辑
@Slf4j
public class JwtUtils {
/**
* 认证服务器许可我们的网关的clientId(需要在oauth_client_details表中配置)
*/
private static final String CLIENT_ID = "api-gateway";
/**
* 认证服务器许可我们的网关的client_secret(需要在oauth_client_details表中配置)
*/
private static final String CLIENT_SECRET = "mall";
/**
* 认证服务器暴露的获取token_key的地址
*/
private static final String AUTH_TOKEN_KEY_URL = "http://mall-authcenter/oauth/token_key";
/**
* 请求头中的 token的开始
*/
private static final String AUTH_HEADER = "bearer ";
/**
* 方法实现说明: 通过远程调用获取认证服务器颁发jwt的解析的key
* @author:smlz
* @param restTemplate 远程调用的操作类
* @return: tokenKey 解析jwt的tokenKey
* @exception:
* @date:2020/1/22 11:31
*/
private static String getTokenKeyByRemoteCall(RestTemplate restTemplate) throws GateWayException {
//第一步:封装请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setBasicAuth(CLIENT_ID,CLIENT_SECRET);
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(null, headers);
//第二步:远程调用获取token_key
try {
ResponseEntity<Map> response = restTemplate.exchange(AUTH_TOKEN_KEY_URL, HttpMethod.GET, entity, Map.class);
String tokenKey = response.getBody().get("value").toString();
log.info("去认证服务器获取Token_Key:{}",tokenKey);
return tokenKey;
}catch (Exception e) {
log.error("远程调用认证服务器获取Token_Key失败:{}",e.getMessage());
throw new GateWayException(ResultCode.GET_TOKEN_KEY_ERROR);
}
}
/**
* 方法实现说明:生成公钥
* @author:smlz
* @param restTemplate:远程调用操作类
* @return: PublicKey 公钥对象
* @exception:
* @date:2020/1/22 11:52
*/
public static PublicKey genPulicKey(RestTemplate restTemplate) throws GateWayException {
String tokenKey = getTokenKeyByRemoteCall(restTemplate);
try{
//把获取的公钥开头和结尾替换掉
String dealTokenKey =tokenKey.replaceAll("\\-*BEGIN PUBLIC KEY\\-*", "").replaceAll("\\-*END PUBLIC KEY\\-*", "").trim();
java.security.Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(dealTokenKey));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
log.info("生成公钥:{}",publicKey);
return publicKey;
}catch (Exception e) {
log.info("生成公钥异常:{}",e.getMessage());
throw new GateWayException(ResultCode.GEN_PUBLIC_KEY_ERROR);
}
}
public static Claims validateJwtToken(String authHeader,PublicKey publicKey) {
String token =null ;
try{
token = StringUtils.substringAfter(authHeader, AUTH_HEADER);
Jwt<JwsHeader, Claims> parseClaimsJwt = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
Claims claims = parseClaimsJwt.getBody();
//log.info("claims:{}",claims);
return claims;
}catch(Exception e){
log.error("校验token异常:{},异常信息:{}",token,e.getMessage());
throw new GateWayException(ResultCode.JWT_TOKEN_EXPIRE);
}
}
}