微服务认证鉴权gateway+oauth2+security+jwt

本文认证鉴权思路方案

微服务认证鉴权gateway+oauth2+security+jwt
实现思路受到开源电商项目mallyoulai-mall启发,此处贴上他们的开源地址

mall: https://gitee.com/macrozheng/mall
youlai-mall: https://gitee.com/youlaitech/youlai-mall

该篇内容主要为了提升自己对oauth2技术的理解、记录走过的坑的一些解决方案以及对网上零散实现方式的整合

用户登录,网关远程调用认证授权服务完成登录,办法token,使用jwt,本文仅为password认证模式,其他模式请了解Oauth的授权模式后自行百度,大同小异

当网关收到客户端请求的时候,验证用户Token是否正确,正确则校验用户是否具备当前请求路径的权限

内部服务之间裸奔,不校验权限

一. 认证服务器

1. 需要依赖

<dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-security</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> 

2. 编写认证服务

package top.sclf.auth.config;  import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; 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.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 top.sclf.auth.domain.CustomUserDetails; import top.sclf.auth.exception.CustomWebResponseExceptionTranslator; import top.sclf.common.core.constant.AuthConstants;  import java.security.KeyPair; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map;  /**  * @author zhangxing  * @date 2021/4/4  */ @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {      private final AuthenticationManager authenticationManager;     private final UserDetailsService userDetailsService;      public AuthorizationServerConfig(             AuthenticationManager authenticationManager,             @Qualifier("userDetailsServiceImpl") UserDetailsService userDetailsService     ) {         this.authenticationManager = authenticationManager;         this.userDetailsService = userDetailsService;     }      @Override     public void configure(ClientDetailsServiceConfigurer clients) throws Exception {     	// 此处客户端可以写死到内存中,也可以从数据库读取,视具体业务而变,客户端作用此处不在讲述         clients.inMemory()                 // 客户端id                 .withClient("client")                 // 客户端密码                 .secret("123456")                 // 自动授权配置                 .autoApprove(true)                 .scopes("all")                 // 客户端授权类型(authorization_code:授权码类型 password:密码类型 implicit:简化类型/隐式类型 client_credentials:客户端类型 refresh_token:该为特例,加了才可以刷新授权)                 .authorizedGrantTypes("password", "refresh_token")                 .accessTokenValiditySeconds(3600 * 24)                 .refreshTokenValiditySeconds(3600 * 24 * 7);     }      @Override     public void configure(AuthorizationServerEndpointsConfigurer endpoints) {         TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();         List<TokenEnhancer> tokenEnhancers = new ArrayList<>();         // 增强jwt载荷的内容         tokenEnhancers.add(tokenEnhancer());         // 添加jwt的加密公钥         tokenEnhancers.add(jwtAccessTokenConverter());         tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);          endpoints.authenticationManager(authenticationManager)                 .accessTokenConverter(jwtAccessTokenConverter())                 .tokenEnhancer(tokenEnhancerChain)                 // 使用自己实现的用户密码校验逻辑                 .userDetailsService(userDetailsService)                 // 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();     }      /**      * jwt生成使用RS256非对称加密,非对称加密需要私钥和公钥,此处设置公钥      */     @Bean     public JwtAccessTokenConverter jwtAccessTokenConverter() {         JwtAccessTokenConverter converter = new JwtAccessTokenConverter();         converter.setKeyPair(keyPair());         return converter;     }      /**      * 从classpath下的密钥库中获取密钥对(公钥+私钥)      */     @Bean     public KeyPair keyPair() {         KeyStoreKeyFactory factory = new KeyStoreKeyFactory(                 new ClassPathResource("jcps.jks"), "123456".toCharArray());         return factory.getKeyPair(                 "jcps", "123456".toCharArray());     }       /**      * JWT内容增强      * 在jwt的载荷中加入自定义的一些内容      */     @Bean     public TokenEnhancer tokenEnhancer() {         return (accessToken, authentication) -> {             Map<String, Object> map = new HashMap<>(1);             CustomUserDetails user = (CustomUserDetails) authentication.getUserAuthentication().getPrincipal();             map.put(AuthConstants.DETAILS_USER_ID, user.getId());             ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);             return accessToken;         };     } }  

PS:jks加密可百度搜索如何生成

以上为认证服务的核心配置
配置好后会自动生成一下几个请求端点

  • /oauth/authorize method=[POST] 授权码类型和隐式类型的授权端点
  • /oauth/token method=[GET,POST] 获取令牌的端点,password模式或有授权code情况下用于获取token
  • /oauth/check_token 请求方式没有测试过,用于检查令牌有效性

3. 安全配置

由于使用Spring Security,故需要开放以上的端点提供给Gateway调用,所以需要添加WebSecurity相关配置

package top.sclf.auth.config;  import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;  /**  * SpringSecurity配置  *  * @author zhangxing  */ @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {      @Override     protected void configure(HttpSecurity http) throws Exception {         http.csrf().disable()                                  // 使用jwt,则无需使用原本的session管理                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)                 .and()                 .authorizeRequests()                 // actuator中的所有健康检查端点都放行,经测试,此处包含了上述几处oauth端点,直接放行                 .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()                 // 此处不加项目的context-path                 .antMatchers("/oauth/logout").permitAll()                 .antMatchers("/getPublicKey").permitAll()                 .anyRequest().authenticated();     }      @Bean     @Override     public AuthenticationManager authenticationManagerBean() throws Exception {     	// 上面的认证服务核心配置需要使用AuthenticationManager来配置UserDetailsService         return super.authenticationManagerBean();     }      @Bean     public PasswordEncoder passwordEncoder() {     	// 测试方便使用密码不加密模式         return NoOpPasswordEncoder.getInstance();         // return new BCryptPasswordEncoder();     }  } 

4. 开放接口配置

关于/getPublicKey端点说明:因为jwt使用RS256非对称加密,非对称加密使用相同的公钥和不同的密钥加密而成,所以在认证服务模块中开放公钥的获取方式

添加对外暴露的退出登录接口和开放公钥接口

package top.sclf.auth.controller;  import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController;  import java.security.KeyPair; import java.security.interfaces.RSAPublicKey; import java.util.Map;  /**  * RSA公钥开放接口  *  * @author zhangxing  * @date 2021/4/5  */ @RestController public class PublicKeyController {      private final KeyPair keyPair;      public PublicKeyController(KeyPair keyPair) {         this.keyPair = keyPair;     }      @GetMapping("/getPublicKey")     public Map<String, Object> getPublicKey() {         RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();         RSAKey key = new RSAKey.Builder(publicKey).build();         return new JWKSet(key).toJSONObject();     }  } 

关于/oauth/logout退出登录的端点的说明
因为JWT本身就是字包含的加密文本,所以不需要在服务端存储Token的过期时间,JWT本身就可以验证自己是否正确,以及什么时候过期,所以意味着Token一旦颁发,从Token本身来说必须等Token本身过期才会失效,为了防止用户退出登录后,Token依旧有效,我们可以在用户退出或者修改密码后将Token加入到Redis中,并设置过期时间,可以理解为将Token加入黑名单,再在gateway上添加过滤器识别token是否在黑名单中即可实现用户的退出改密作废Token的功能

package top.sclf.auth.controller;  import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import top.sclf.common.core.constant.AuthConstants; import top.sclf.common.core.http.ResultEntity;  import javax.servlet.http.HttpServletRequest; import java.security.Principal; import java.util.Map; import java.util.concurrent.TimeUnit;  /**  * 自定义Oauth2获取令牌接口  *  * @author zhangxing  */ @RestController @RequestMapping("/oauth") public class AuthController {      @Autowired     private TokenEndpoint tokenEndpoint;     @Autowired     private RedisTemplate<String, Object> redisTemplate;      @PostMapping("/token")     public ResultEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {         OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();         return ResultEntity.ok(oAuth2AccessToken);     }      @DeleteMapping("/logout")     public ResultEntity<?> logout(HttpServletRequest request) {         String payload = request.getHeader(AuthConstants.USER_TOKEN_HEADER);         JSONObject jsonObject = JSONUtil.parseObj(payload);          // JWT唯一标识         String jti = jsonObject.getStr("jti");         // JWT过期时间戳(单位:秒)         long exp = jsonObject.getLong("exp");          long currentTimeSeconds = System.currentTimeMillis() / 1000;          // token已过期         if (exp < currentTimeSeconds) {             return ResultEntity.fail("登录凭证超时");         }         redisTemplate.opsForValue().set(AuthConstants.TOKEN_BLACKLIST_PREFIX + jti, null, (exp - currentTimeSeconds), TimeUnit.SECONDS);         return ResultEntity.ok();     } } 

为什么这里又重写了获取/oauth/token端点呢,是为了通过@RestControllerAdvice来捕捉异常

添加异常捕获

/**  * @author zhangxing  */ @ControllerAdvice public class Oauth2ExceptionHandler {     @ResponseBody     @ExceptionHandler(value = OAuth2Exception.class)     public ResultEntity<?> handleOauth2(OAuth2Exception e) {         return ResultEntity.fail(e.getMessage());     } } 

添加认证中查询用户的实现

package top.sclf.auth.service;  import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import top.sclf.api.resource.RemoteResUserService; import top.sclf.api.resource.domain.model.LoginUser; import top.sclf.auth.constant.MessageConstant; import top.sclf.auth.domain.CustomUserDetails; import top.sclf.common.core.enums.DelFlagEnum; import top.sclf.common.core.http.ResultEntity;  import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional;  /**  * @author zhangxing  * @date 2021/4/4  */ @Service public class UserDetailsServiceImpl implements UserDetailsService {  	// 远程调用用户服务     @Autowired     private RemoteResUserService remoteResUserService;      @Override     public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {         ResultEntity<LoginUser> loginUserRes = remoteResUserService.getByLoginName(loginName);         LoginUser.User user = Optional.ofNullable(loginUserRes)                 .map(ResultEntity::getData)                 .map(LoginUser::getResUser)                 .orElse(null);         if (user == null) {             throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR);         }         CustomUserDetails userDetails = new CustomUserDetails();         userDetails.setId(user.getId());         userDetails.setLoginName(user.getLoginName());         userDetails.setUserName(user.getUserName());         userDetails.setPassword(user.getLoginPwd());         userDetails.setEnable(Objects.equals(user.getDelFlag(), DelFlagEnum.DEFAULT.getVal()));  		// 此处查询系统中用户的角色权限等信息         List<CustomUserDetails.Perm> perms = new ArrayList<CustomUserDetails.Perm>(){{             add(new CustomUserDetails.Perm("/a"));             add(new CustomUserDetails.Perm("/b"));         }};          userDetails.setPermList(perms);          return userDetails;     } } 
package top.sclf.auth.domain;  import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Data; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails;  import java.io.Serializable; import java.util.Collection; import java.util.List;  /**  * @author zhangxing  * @date 2021/4/4  */ @Data public class CustomUserDetails implements UserDetails, Serializable {      private static final long serialVersionUID = 1L;      private Long id;      private String loginName;      private String userName;      @JsonIgnore     private String password;      private boolean enable;      private List<Perm> permList;      @Data     @AllArgsConstructor     public static class Perm implements GrantedAuthority {          private String uri;          @Override         public String getAuthority() {             return uri;         }     }      @Override     public Collection<? extends GrantedAuthority> getAuthorities() {         return permList;     }      @Override     public String getPassword() {         return this.password;     }      @Override     public String getUsername() {         return this.loginName;     }      @Override     public boolean isAccountNonExpired() {         return true;     }      @Override     public boolean isAccountNonLocked() {         return true;     }      @Override     public boolean isCredentialsNonExpired() {         return true;     }      @Override     public boolean isEnabled() {         return enable;     } } 

二. 资源服务器(此处可理解为鉴权服务)

1. 需要依赖

<dependency> 	<groupId>org.springframework.security</groupId> 	<artifactId>spring-security-config</artifactId> </dependency> <dependency> 	<groupId>org.springframework.security</groupId> 	<artifactId>spring-security-oauth2-resource-server</artifactId> </dependency> <dependency> 	<groupId>org.springframework.security</groupId> 	<artifactId>spring-security-oauth2-jose</artifactId> </dependency> 

2. 编写鉴权管理器

package top.sclf.gateway.config;  import cn.hutool.core.convert.Convert; import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpMethod; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.server.authorization.AuthorizationContext; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import reactor.core.publisher.Mono; import top.sclf.common.core.constant.AuthConstants;  import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map;  /**  * 鉴权管理器  *  * @author zhangxing  */ @Slf4j @Component public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {      private final RedisTemplate redisTemplate;      public AuthorizationManager(RedisTemplate redisTemplate) {         this.redisTemplate = redisTemplate;     }      @Override     public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {         ServerHttpRequest request = authorizationContext.getExchange().getRequest();         String path = request.getURI().getPath();         PathMatcher pathMatcher = new AntPathMatcher();          // 1. 对应跨域的预检请求直接放行         if (request.getMethod() == HttpMethod.OPTIONS) {             return Mono.just(new AuthorizationDecision(true));         }          // 2. token为空拒绝访问         String token = request.getHeaders().getFirst(AuthConstants.HEADER);         if (StrUtil.isBlank(token)) {             return Mono.just(new AuthorizationDecision(false));         }          // 3.缓存取资源权限角色关系列表         // Map<Object, Object> resourceRolesMap = redisTemplate.opsForHash().entries(AuthConstants.RESOURCE_ROLES_KEY);         Map<Object, Object> resourceRolesMap = new HashMap<>(0);         Iterator<Object> iterator = resourceRolesMap.keySet().iterator();          // 4.请求路径匹配到的资源需要的角色权限集合authorities         List<String> authorities = new ArrayList<>();         while (iterator.hasNext()) {             String pattern = (String) iterator.next();             if (pathMatcher.match(pattern, path)) {                 authorities.addAll(Convert.toList(String.class, resourceRolesMap.get(pattern)));             }         }         return mono                 .filter(Authentication::isAuthenticated)                 .flatMapIterable(Authentication::getAuthorities)                 .map(GrantedAuthority::getAuthority)                 .any(roleId -> {                     // 5. roleId是请求用户的角色(格式:ROLE_{roleId}),authorities是请求资源所需要角色的集合                     log.info("访问路径:{}", path);                     log.info("用户角色roleId:{}", roleId);                     log.info("资源需要权限authorities:{}", authorities);                     // return authorities.contains(roleId);                     return true;                 })                 .map(AuthorizationDecision::new)                 .defaultIfEmpty(new AuthorizationDecision(false));     } } 

以上的鉴权逻辑参考的mall项目,可以根据自己的业务修改

3. 编写资源服务

资源服务需要申明哪些资源需要被保护起来,哪些资源放行,以及被保护起来的资源的保护逻辑(鉴权管理器)

package top.sclf.gateway.config;   import cn.hutool.core.util.ArrayUtil; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter; import org.springframework.security.web.server.SecurityWebFilterChain; import reactor.core.publisher.Mono; import top.sclf.gateway.filter.BlackListFilter; import top.sclf.gateway.handler.CustomServerAccessDeniedHandler; import top.sclf.gateway.handler.CustomServerAuthenticationEntryPoint; import top.sclf.gateway.properties.IgnoreWhiteProperties;  /**  * 资源服务器配置  *  * @author zhangxing  */ @Configuration @EnableWebFluxSecurity public class ResourceServerConfig {      private final AuthorizationManager authorizationManager;     private final CustomServerAccessDeniedHandler customServerAccessDeniedHandler;     private final CustomServerAuthenticationEntryPoint customServerAuthenticationEntryPoint;     private final IgnoreWhiteProperties ignoreWhiteProperties;     private final BlackListFilter blackListFilter;      public ResourceServerConfig(AuthorizationManager authorizationManager, CustomServerAccessDeniedHandler customServerAccessDeniedHandler, CustomServerAuthenticationEntryPoint customServerAuthenticationEntryPoint, IgnoreWhiteProperties ignoreWhiteProperties, BlackListFilter blackListFilter) {         this.authorizationManager = authorizationManager;         this.customServerAccessDeniedHandler = customServerAccessDeniedHandler;         this.customServerAuthenticationEntryPoint = customServerAuthenticationEntryPoint;         this.ignoreWhiteProperties = ignoreWhiteProperties;         this.blackListFilter = blackListFilter;     }      @Bean     public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {         http.oauth2ResourceServer().jwt()                 .jwtAuthenticationConverter(jwtAuthenticationConverter());         // 自定义处理JWT请求头过期或签名错误的结果         http.oauth2ResourceServer().authenticationEntryPoint(customServerAuthenticationEntryPoint);  		// 在鉴权之前,添加一个黑名单过滤器,即认证服务中说到的用户退出或修改密码后,应该将Token加入黑名单,如果Token已经在黑名单中了,则由该Token发起的请求也无需再做鉴权判断了,所以黑名单过滤器必须在鉴权之前,所以放在了认证过滤器的前面         http.addFilterBefore(blackListFilter, SecurityWebFiltersOrder.AUTHENTICATION);         http.authorizeExchange()         		// 在网关上放行的请求,该白名单为List<String>,可自行配置,必须包含如下两个请求         		// /oauth/login,/getPublicKey                 .pathMatchers(ArrayUtil.toArray(ignoreWhiteProperties.getWhites(), String.class)).permitAll()                 // 认证通过后即可发起退出登录请求                 .pathMatchers("/auth/oauth/logout").authenticated()                 // 鉴权管理器,剩下的请求通过鉴权管理器判定                 .anyExchange().access(authorizationManager)                 .and()                 // 添加异常处理的响应                 .exceptionHandling()                 // 处理未授权                 .accessDeniedHandler(customServerAccessDeniedHandler)                 // 处理未认证                 .authenticationEntryPoint(customServerAuthenticationEntryPoint)                 // csrf自行百度                 .and().csrf().disable();          return http.build();     }       /**      * ServerHttpSecurity没有将jwt中authorities的负载部分当做Authentication      * 需要把jwt的Claim中的authorities加入      * 方案:重新定义ReactiveAuthenticationManager权限管理器,默认转换器JwtGrantedAuthoritiesConverter      */     @Bean     public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {         JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();         // 将认证服务中返回的用户对象信息保存在JWT中,即自主实现的UserDetailsService返回的对象权限加载到JWT中         // UserDetailsService返回对象中的权限添加到jwt中,并加上前缀         jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");         // 通过authorities字段从jwt中获取UserDetailsService返回的对象中的权限         jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");          JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();         jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);         return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);     }  } 

3. 黑名单过滤器

package top.sclf.gateway.filter;  import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.nimbusds.jose.JWSObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; import top.sclf.common.core.constant.AuthConstants; import top.sclf.common.core.exception.CustomException; import top.sclf.common.core.http.ResultEntity; import top.sclf.common.core.http.ResultEnum;  import java.nio.charset.StandardCharsets; import java.text.ParseException;  /**  * @author zhangxing  * @date 2021/4/7  */ @Component public class BlackListFilter implements WebFilter {      @Autowired     private RedisTemplate<String, Object> redisTemplate;      @Override     public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {         String token = exchange.getRequest().getHeaders().getFirst(AuthConstants.HEADER);         if (StrUtil.isEmpty(token)) {             return chain.filter(exchange);         }         String realToken = token.replace("Bearer ", "");         JWSObject jwsObject;         try {             // 从token中解析用户信息并设置到Header中去             jwsObject = JWSObject.parse(realToken);         } catch (ParseException e) {             throw new CustomException(ResultEnum.SERVER_ERROR, "token解析错误");         }         String payloadStr = jwsObject.getPayload().toString();          JSONObject payload = JSONUtil.parseObj(payloadStr);          // 校验该token是否存在于黑名单中(登出、修改密码)         // JWT唯一标识         String jti = payload.getStr("jti");         Boolean isBlack = redisTemplate.hasKey(AuthConstants.TOKEN_BLACKLIST_PREFIX + jti);         if (Boolean.TRUE.equals(isBlack)) {             ServerHttpResponse response = exchange.getResponse();             response.setStatusCode(HttpStatus.OK);             response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);             response.getHeaders().set("Access-Control-Allow-Origin", "*");             response.getHeaders().set("Cache-Control", "no-cache");             String body = JSONUtil.toJsonStr(ResultEntity.fail(ResultEnum.TOKEN_EXPIRED, ResultEnum.TOKEN_EXPIRED.getMessage()));             DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));             return response.writeWith(Mono.just(buffer));         }          return chain.filter(exchange);     } } 

4. 异常处理

package top.sclf.gateway.handler;  import cn.hutool.json.JSONUtil; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import top.sclf.common.core.http.ResultEntity; import top.sclf.common.core.http.ResultEnum;  import java.nio.charset.StandardCharsets;  /**  * 无权访问自定义响应  */ @Component public class CustomServerAccessDeniedHandler implements ServerAccessDeniedHandler {      @Override     public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException e) {         ServerHttpResponse response = exchange.getResponse();         response.setStatusCode(HttpStatus.OK);         response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);         response.getHeaders().set("Access-Control-Allow-Origin", "*");         response.getHeaders().set("Cache-Control", "no-cache");         String body = JSONUtil.toJsonStr(ResultEntity.fail(ResultEnum.NOT_PERMISSION.getCode(), ResultEnum.NOT_PERMISSION.getMessage()));         DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));         return response.writeWith(Mono.just(buffer));     } } 
package top.sclf.gateway.handler;  import cn.hutool.json.JSONUtil; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.server.ServerAuthenticationEntryPoint; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import top.sclf.common.core.http.ResultEntity; import top.sclf.common.core.http.ResultEnum; import top.sclf.common.core.util.StringUtils;  import java.nio.charset.StandardCharsets;  /**  * 无效token/token过期 自定义响应  *  * @author zhangxing  */ @Component public class CustomServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {      @Override     public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {          ServerHttpResponse response = exchange.getResponse();         response.setStatusCode(HttpStatus.OK);         response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);         response.getHeaders().set("Access-Control-Allow-Origin", "*");         response.getHeaders().set("Cache-Control", "no-cache");         ResultEntity<String> fail = ResultEntity.fail(ResultEnum.TOKEN_INVALID.getCode().intValue(), "未登陆或登录已过期");  		// 文章下面会详细描述为什么此处需要判断是否是jwt过期的情况         String message = e.getMessage();         if (message != null && StringUtils.containsIgnoreCase(message, "Jwt expired")) {             fail.setCode(ResultEnum.TOKEN_EXPIRED.getCode());         }          String body = JSONUtil.toJsonStr(fail);         DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));         return response.writeWith(Mono.just(buffer));     }  } 

5. JWT刷新方案

权限校验失败的自定义响应中,我们判断了是否是应为jwt过期导致的鉴权失败
当请求进来时,如果jwt过期,我们定制一个和前端约定好的错误编码来表示jwt过期,当前端请求访问失败,并且发现响应编码是因为jwt过期导致的请求失败,则前端使用refresh_token使用刷新token的请求方式来重新获取一次新的授权JWT,返回新JWT后重新设置到请求头上再次发起刚才鉴权失败的请求即可

6. 配置网关模块调用认证模块获取jwt加密公钥地址

在网关模块的配置文件中加入

spring:   security:     oauth2:       resourceserver:         jwt: 		  # 网上找了一圈没有找到此处配置负载均衡的方式,似乎不支持 		  # 所以此处配置的获取公钥的地址直接访问的网关的地址,曲线救国的方式实现负载均衡           jwk-set-uri: http://localhost:8088/auth/getPublicKey # /auth是我的认证模块的context-path 

三. 配置完毕,开始测试

1. 获取Token

登录获取Token端点: POST /oauth/token,/auth是我的认证模块context-path
微服务认证鉴权gateway+oauth2+security+jwt
返回值:

  • access_token用于访问资源
  • refresh_token用于刷新token,以获取新的access_token

2. 刷新Token

刷新Token端点: POST /oauth/token,/auth是我的认证模块context-path
微服务认证鉴权gateway+oauth2+security+jwt

3. 携带Token访问资源

微服务认证鉴权gateway+oauth2+security+jwt

4. 退出登录

微服务认证鉴权gateway+oauth2+security+jwt

5. 退出登录后再次访问资源

微服务认证鉴权gateway+oauth2+security+jwt

版权声明:玥玥 发表于 2021-04-09 14:26:41。
转载请注明:微服务认证鉴权gateway+oauth2+security+jwt | 女黑客导航