微服务实现统一认证和鉴权
在我们的电商系统开发过程中,无论是管理后台还是APP客户端都需要将请求发送至网关,由网关来进行分发,但是在这过程中可能会遇到非法的请求或者权限越界等行为,鉴于此,我们需要在网关层就实现统一的认证与鉴权,研究了一天左右,终于在自己的项目中实现了网关层的鉴权,分享经验给大家。
一、搭建认证服务器
1.1 引入pom依赖
<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>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.16</version>
</dependency>
1.2 修改yml文件
spring:
datasource:
password:
username:
url:
redis:
database: 0
port: 6379
host: localhost
oauth2:
jwtPassword: 123456
authorizedGrantTypes:
- "password"
- "refresh_token"
scopes: all
secret: 123456
accessTokenValiditySeconds: 3600 # 配置访问token的有效期
refreshTokenValiditySeconds: 3600 # 配置刷新token的有效期
1.3 正式编码
1.3.1 定义权限相关的常量
package com.bdysoft.shop.auth.constant;
/**
* 权限相关的常量
*
* @author lvwei
*/
public class AuthConstant {
/**
* JWT存储权限前缀
*/
public static final String AUTHORITY_PREFIX = "ROLE_";
/**
* JWT存储权限属性
*/
public static final String AUTHORITY_CLAIM_NAME = "authorities";
/**
* 后台管理client_id
*/
public static final String ADMIN_CLIENT_ID = "shop-admin";
/**
* 前台商城client_id
*/
public static final String PORTAL_CLIENT_ID = "shop-app";
/**
* 后台管理接口路径匹配
*/
public static final String ADMIN_URL_PATTERN = "/shop-admin/**";
/**
* Redis缓存权限规则key
*/
public static final String RESOURCE_ROLES_MAP_KEY = "auth:resourceRolesMap";
/**
* 认证信息Http请求头
*/
public static final String JWT_TOKEN_HEADER = "Authorization";
/**
* JWT令牌前缀
*/
public static final String JWT_TOKEN_PREFIX = "Shop ";
/**
* 用户信息Http请求头
*/
public static final String USER_TOKEN_HEADER = "user";
}
1.3.2 定义消息相关的常量
package com.bdysoft.shop.auth.constant;
/**
* 消息常量
*/
public class MessageConstant {
public static final String LOGIN_SUCCESS = "登录成功!";
public static final String USERNAME_PASSWORD_ERROR = "用户名或密码错误!";
public static final String CREDENTIALS_EXPIRED = "该账户的登录凭证已过期,请重新登录!";
public static final String ACCOUNT_DISABLED = "该账户已被禁用,请联系管理员!";
public static final String ACCOUNT_LOCKED = "该账号已被锁定,请联系管理员!";
public static final String ACCOUNT_EXPIRED = "该账号已过期,请联系管理员!";
public static final String PERMISSION_DENIED = "没有访问权限,请联系管理员!";
}
1.3.3 创建用户信息
import java.util.List;
/**
* 登录用户信息
*
* @author lvwei
*/
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
public class UserDto {
private Long id;
private String userName;
private String realName;
private String password;
private Integer status;
private Integer storeId;
private Boolean isSupper;
private String clientId;
private List<String> roles;
}
1.3.4 创建登录用户信息
package com.bdysoft.shop.auth.dto;
import com.bdysoft.shop.common.domain.UserDto;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Collection;
/**
* 登录用户信息
*/
@Data
public class SecurityUser implements UserDetails {
private Long id;
private String userName;
private String realName;
private String password;
private Boolean enabled;
private Boolean isSupper;
private Integer storeId;
private String clientId;
private Collection<SimpleGrantedAuthority> authorities;
public SecurityUser() {
}
public SecurityUser(UserDto userDto) {
this.id = userDto.getId();
this.userName = userDto.getUserName();
this.realName = userDto.getRealName();
this.password = userDto.getPassword();
this.enabled = userDto.getStatus() == 1;
this.storeId = userDto.getStoreId();
this.clientId = userDto.getClientId();
this.isSupper = userDto.getIsSuper();
if (!CollectionUtils.isEmpty(userDto.getRoles())) {
authorities = new ArrayList<>();
userDto.getRoles().forEach(item -> authorities.add(new SimpleGrantedAuthority(item)));
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.userName;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
1.3.5 创建用户管理服务类
package com.bdysoft.shop.auth.service.impl;
import com.bdysoft.shop.auth.constant.AuthConstant;
import com.bdysoft.shop.auth.constant.MessageConstant;
import com.bdysoft.shop.auth.dto.SecurityUser;
import com.bdysoft.shop.auth.feign.AdminUserService;
import com.bdysoft.shop.auth.feign.AppUserService;
import com.bdysoft.shop.common.domain.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import javax.servlet.http.HttpServletRequest;
/**
* 用户管理服务类
*
* @author lvwei
*/
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private HttpServletRequest request;
@Autowired
private AdminUserService adminUserService;
@Autowired
private AppUserService appUserService;
/**
* 通过用户名加载用户信息
* 在这里我们针对不同的客户端进行处理
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String clientId = request.getParameter("client_id");
String storeId = request.getParameter("store_id");
UserDto userDto;
if (AuthConstant.ADMIN_CLIENT_ID.equals(clientId)) {
userDto = adminUserService.loadUserByUsername(username, Long.parseLong(storeId));
} else {
userDto = appUserService.loadUserByUsername(username, Long.parseLong(storeId));
}
if (userDto == null) {
throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR);
}
userDto.setClientId(clientId);
userDto.setStoreId(Integer.parseInt(storeId));
SecurityUser securityUser = new SecurityUser(userDto);
if (!securityUser.isEnabled()) {
throw new DisabledException(MessageConstant.ACCOUNT_DISABLED);
} else if (!securityUser.isAccountNonLocked()) {
throw new LockedException(MessageConstant.ACCOUNT_LOCKED);
} else if (!securityUser.isAccountNonExpired()) {
throw new AccountExpiredException(MessageConstant.ACCOUNT_EXPIRED);
} else if (!securityUser.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED);
}
return securityUser;
}
}
1.3.6 往JWT中添加自定义信息
/**
* JWT内容增强器
* 添加自定义用户信息
* @author lvwei
*/
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
Map<String, Object> info = new HashMap<>(3);
//把用户ID设置到JWT中
info.put("user_id", securityUser.getId());
info.put("client_id", securityUser.getClientId());
info.put("store_id", securityUser.getStoreId());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}
1.3.7 添加授权私有化配置
package com.bdysoft.shop.auth.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "oauth2")
public class Oauth2ConfigProperties {
private String scopes;
private String secret;
private Integer accessTokenValiditySeconds;
private Integer refreshTokenValiditySeconds;
private String[] authorizedGrantTypes;
private String jwtPassword;
public String getScopes() {
return scopes;
}
public void setScopes(String scopes) {
this.scopes = scopes;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public Integer getAccessTokenValiditySeconds() {
return accessTokenValiditySeconds;
}
public void setAccessTokenValiditySeconds(Integer accessTokenValiditySeconds) {
this.accessTokenValiditySeconds = accessTokenValiditySeconds;
}
public Integer getRefreshTokenValiditySeconds() {
return refreshTokenValiditySeconds;
}
public void setRefreshTokenValiditySeconds(Integer refreshTokenValiditySeconds) {
this.refreshTokenValiditySeconds = refreshTokenValiditySeconds;
}
public String[] getAuthorizedGrantTypes() {
return authorizedGrantTypes;
}
public void setAuthorizedGrantTypes(String[] authorizedGrantTypes) {
this.authorizedGrantTypes = authorizedGrantTypes;
}
public String getJwtPassword() {
return jwtPassword;
}
public void setJwtPassword(String jwtPassword) {
this.jwtPassword = jwtPassword;
}
}
1.3.8 认证服务器配置
使用@EnableAuthorizationServer注解开启
package com.bdysoft.shop.auth.config;
import com.bdysoft.shop.auth.component.JwtTokenEnhancer;
import com.bdysoft.shop.auth.constant.AuthConstant;
import com.bdysoft.shop.auth.properties.Oauth2ConfigProperties;
import com.bdysoft.shop.auth.service.impl.UserDetailServiceImpl;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
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.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.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.List;
/**
* 认证服务器配置
*
* @author lvwei
*/
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
private final PasswordEncoder passwordEncoder;
private final UserDetailServiceImpl userDetailsService;
private final AuthenticationManager authenticationManager;
private final JwtTokenEnhancer jwtTokenEnhancer;
@Autowired
private Oauth2ConfigProperties oauth2ConfigProperties;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(AuthConstant.ADMIN_CLIENT_ID)
.secret(passwordEncoder.encode(oauth2ConfigProperties.getSecret()))
.scopes(oauth2ConfigProperties.getScopes())
.authorizedGrantTypes(oauth2ConfigProperties.getAuthorizedGrantTypes())
.accessTokenValiditySeconds(oauth2ConfigProperties.getAccessTokenValiditySeconds())
.refreshTokenValiditySeconds(oauth2ConfigProperties.getRefreshTokenValiditySeconds())
.and()
.withClient(AuthConstant.PORTAL_CLIENT_ID)
.secret(passwordEncoder.encode(oauth2ConfigProperties.getSecret()))
.scopes(oauth2ConfigProperties.getScopes())
.authorizedGrantTypes(oauth2ConfigProperties.getAuthorizedGrantTypes())
.accessTokenValiditySeconds(oauth2ConfigProperties.getAccessTokenValiditySeconds())
.refreshTokenValiditySeconds(oauth2ConfigProperties.getRefreshTokenValiditySeconds());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> delegates = new ArrayList<>();
delegates.add(jwtTokenEnhancer);
delegates.add(accessTokenConverter());
enhancerChain.setTokenEnhancers(delegates);
//配置JWT的内容增强器
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
//配置加载用户信息的服务
.accessTokenConverter(accessTokenConverter())
.tokenEnhancer(enhancerChain);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setKeyPair(keyPair());
return jwtAccessTokenConverter;
}
@Bean
public KeyPair keyPair() {
//从classpath下的证书中获取秘钥对
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), oauth2ConfigProperties.getJwtPassword().toCharArray());
return keyStoreKeyFactory.getKeyPair("jwt", oauth2ConfigProperties.getJwtPassword().toCharArray());
}
}
1.3.9 SpringSecurity配置
添加SpringSecurity配置,允许认证相关路径的访问及表单登录
package com.bdysoft.shop.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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* SpringSecurity配置
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
.antMatchers("/rsa/publicKey").permitAll()
.antMatchers("/v2/api-docs").permitAll()
.anyRequest().authenticated();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
1.3.10 自定义Token返回结口
package com.bdysoft.shop.auth.dto;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Oauth2获取Token返回信息封装
* @author lvwei
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Builder
public class Oauth2TokenDto {
private String token;
private String refreshToken;
private String tokenHead;
private int expiresIn;
}
1.3.11 获取RSA公钥接口
package com.bdysoft.shop.auth.controller;
import com.bdysoft.shop.auth.constant.AuthConstant;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import org.springframework.beans.factory.annotation.Autowired;
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.*;
import java.security.KeyPair;
import java.security.Principal;
import java.security.interfaces.RSAPublicKey;
import java.util.Map;
/**
* 获取RSA公钥接口
* @author lvwei
*/
@RestController=
public class KeyPairController {
@Autowired
private KeyPair keyPair;
@GetMapping("/rsa/publicKey")
public Map<String, Object> getKey() {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAKey key = new RSAKey.Builder(publicKey).build();
return new JWKSet(key).toJSONObject();
}
}
1.3.12 自定义Oauth2获取令牌接口
package com.bdysoft.shop.auth.controller;
import com.bdysoft.shop.auth.constant.AuthConstant;
import com.bdysoft.shop.auth.dto.Oauth2TokenDto;
import com.bdysoft.shop.common.result.BaseResult;
import org.springframework.beans.factory.annotation.Autowired;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
import java.util.Map;
/**
* 自定义Oauth2获取令牌接口
*/
@RestController
@RequestMapping("/oauth")
public class AuthController {
@Autowired
private TokenEndpoint tokenEndpoint;
@RequestMapping(value = "/token", method = RequestMethod.POST)
public BaseResult postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder()
.token(oAuth2AccessToken.getValue())
.refreshToken(oAuth2AccessToken.getRefreshToken().getValue())
.expiresIn(oAuth2AccessToken.getExpiresIn())
.tokenHead(AuthConstant.JWT_TOKEN_PREFIX).build();
return BaseResult.ok().data(oauth2TokenDto);
}
}
1.3.11 Oauth2ConfigProperties配置
package com.bdysoft.shop.auth.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "oauth2")
public class Oauth2ConfigProperties {
private String scopes;
private String secret;
private Integer accessTokenValiditySeconds;
private Integer refreshTokenValiditySeconds;
private String[] authorizedGrantTypes;
private String jwtPassword;
public String getScopes() {
return scopes;
}
public void setScopes(String scopes) {
this.scopes = scopes;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public Integer getAccessTokenValiditySeconds() {
return accessTokenValiditySeconds;
}
public void setAccessTokenValiditySeconds(Integer accessTokenValiditySeconds) {
this.accessTokenValiditySeconds = accessTokenValiditySeconds;
}
public Integer getRefreshTokenValiditySeconds() {
return refreshTokenValiditySeconds;
}
public void setRefreshTokenValiditySeconds(Integer refreshTokenValiditySeconds) {
this.refreshTokenValiditySeconds = refreshTokenValiditySeconds;
}
public String[] getAuthorizedGrantTypes() {
return authorizedGrantTypes;
}
public void setAuthorizedGrantTypes(String[] authorizedGrantTypes) {
this.authorizedGrantTypes = authorizedGrantTypes;
}
public String getJwtPassword() {
return jwtPassword;
}
public void setJwtPassword(String jwtPassword) {
this.jwtPassword = jwtPassword;
}
}
二、搭建微服务网关
我们就可以搭建网关服务了,它将作为Oauth2的资源服务、客户端服务使用,对访问微服务的请求进行统一的校验认证和鉴权操作
1.1 引入pom依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<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-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>${nimbus-jose-jwt.version}</version>
</dependency>
<dependency>
<groupId>com.bdysoft</groupId>
<artifactId>shop-cloud-framework</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
<exclusion>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
1.2 修改yml文件
spring:
datasource:
password:
username:
url:
cloud:
nacos:
discovery:
server-addr:
gateway:
routes: #配置路由路径
- id: shop-cloud-oauth2-api-route
uri: lb://shop-cloud-oauth2-api
predicates:
- Path=/api/**
filters:
- StripPrefix=1
- id: shop-cloud-oauth2-auth-route
uri: lb://shop-cloud-oauth2-auth
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能
lower-case-service-id: true #使用小写服务名,默认是大写
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: 'http://localhost:9001/auth/rsa/publicKey' #配置RSA的公钥访问地址
redis:
database: 0
port: 6379
host: localhost
secure:
ignore:
urls: #配置白名单路径
- "/actuator/**"
- "/auth/oauth/token"
1.3 正式编码
1.3.1 全局跨域配置
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
1.3.2 配置网关白名单
@Data
@EqualsAndHashCode(callSuper = false)
@Component
@ConfigurationProperties(prefix="secure.ignore")
public class IgnoreUrlsConfig {
private List<String> urls;
}
1.3.3 自定义没有权限访问处理器
@Component
public class RestfulAccessDeniedHandler implements ServerAccessDeniedHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
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(BaseResult.error(denied.getMessage()));
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
return response.writeWith(Mono.just(buffer));
}
}
3.4 自定义没有登录或token过期时处理器
@Component
public class RestAuthenticationEntryPoint 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");
String body = JSONUtil.toJsonStr(BaseResult.error(e.getMessage()));
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
return response.writeWith(Mono.just(buffer));
}
}
1.3.5 鉴权管理器,用于判断是否有资源的访问权限
/**
* 鉴权管理器,用于判断是否有资源的访问权限
* @author lvwei
*/
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
URI uri = request.getURI();
PathMatcher pathMatcher = new AntPathMatcher();
//白名单路径直接放行
List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
for (String ignoreUrl : ignoreUrls) {
if (pathMatcher.match(ignoreUrl, uri.getPath())) {
return Mono.just(new AuthorizationDecision(true));
}
}
//对应跨域的预检请求直接放行
if(request.getMethod()== HttpMethod.OPTIONS){
return Mono.just(new AuthorizationDecision(true));
}
//不同用户体系登录不允许互相访问
try {
String token = request.getHeaders().getFirst(AuthConstant.JWT_TOKEN_HEADER);
if(StrUtil.isEmpty(token)){
return Mono.just(new AuthorizationDecision(false));
}
String realToken = token.replace(AuthConstant.JWT_TOKEN_PREFIX, "");
JWSObject jwsObject = JWSObject.parse(realToken);
String userStr = jwsObject.getPayload().toString();
UserDto userDto = JSONUtil.toBean(userStr, UserDto.class);
if (AuthConstant.ADMIN_CLIENT_ID.equals(userDto.getClientId()) && !pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())) {
return Mono.just(new AuthorizationDecision(false));
}
if (AuthConstant.PORTAL_CLIENT_ID.equals(userDto.getClientId()) && pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())) {
return Mono.just(new AuthorizationDecision(false));
}
} catch (ParseException e) {
e.printStackTrace();
return Mono.just(new AuthorizationDecision(false));
}
//非管理端路径直接放行
if (!pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())) {
return Mono.just(new AuthorizationDecision(true));
}
//管理端路径需校验权限
Map<Object, Object> resourceRolesMap = redisTemplate.opsForHash().entries(AuthConstant.RESOURCE_ROLES_MAP_KEY);
Iterator<Object> iterator = resourceRolesMap.keySet().iterator();
List<String> authorities = new ArrayList<>();
while (iterator.hasNext()) {
String pattern = (String) iterator.next();
if (pathMatcher.match(pattern, uri.getPath())) {
authorities.addAll(Convert.toList(String.class, resourceRolesMap.get(pattern)));
}
}
authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList());
//认证通过且角色匹配的用户可访问当前路径
return mono
.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
.any(authorities::contains)
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));
}
}
1.3.6 网关服务配置安全配置
Gateway使用的是WebFlux
,所以需要使用@EnableWebFluxSecurity
注解开启
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
private final IgnoreUrlsConfig ignoreUrlsConfig;
private final AuthorizationManager authorizationManager;
private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
http.authorizeExchange()
.pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(), String.class)).permitAll()//白名单配置
.anyExchange().access(authorizationManager)//鉴权管理器配置
.and().exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)//处理未授权
.authenticationEntryPoint(restAuthenticationEntryPoint)//处理未认证
.and().csrf().disable();
return http.build();
}
@Bean
public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX);
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME);
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
}
1.3.7 全局过滤器
实现一个全局过滤器AuthGlobalFilter
,当鉴权通过后将JWT令牌中的用户信息解析出来,然后存入请求的Header中,这样后续服务就不需要解析JWT令牌了,可以直接从请求的Header中获取到用户信息。
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst(AuthConstant.JWT_TOKEN_HEADER);
if (StrUtil.isEmpty(token)) {
return chain.filter(exchange);
}
try {
//从token中解析用户信息并设置到Header中去
String realToken = token.replace(AuthConstant.JWT_TOKEN_PREFIX, "");
JWSObject jwsObject = JWSObject.parse(realToken);
String userStr = jwsObject.getPayload().toString();
LOGGER.info("AuthGlobalFilter.filter() user:{}",userStr);
ServerHttpRequest request = exchange.getRequest().mutate().header(AuthConstant.USER_TOKEN_HEADER, userStr).build();
exchange = exchange.mutate().request(request).build();
} catch (ParseException e) {
e.printStackTrace();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
1.3.8 白名单路径访问时需要移除JWT请求头
/**
* 白名单路径访问时需要移除JWT请求头
*/
@Component
public class IgnoreUrlsRemoveJwtFilter implements WebFilter {
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
URI uri = request.getURI();
PathMatcher pathMatcher = new AntPathMatcher();
//白名单路径移除JWT请求头
List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
for (String ignoreUrl : ignoreUrls) {
if (pathMatcher.match(ignoreUrl, uri.getPath())) {
request = exchange.getRequest().mutate().header(AuthConstant.JWT_TOKEN_HEADER, "").build();
exchange = exchange.mutate().request(request).build();
return chain.filter(exchange);
}
}
return chain.filter(exchange);
}
}
评论 (0)