最近發(fā)現(xiàn)了一個(gè)很好的微服務(wù)權(quán)限解決方案漩蟆,可以通過認(rèn)證服務(wù)進(jìn)行統(tǒng)一認(rèn)證,然后通過網(wǎng)關(guān)來統(tǒng)一校驗(yàn)認(rèn)證和鑒權(quán)妓蛮。此方案為目前最新方案怠李,僅支持Spring Boot 2.2.0、Spring Cloud Hoxton 以上版本仔引,本文將詳細(xì)介紹該方案的實(shí)現(xiàn),希望對(duì)大家有所幫助褐奥!
SpringBoot實(shí)戰(zhàn)電商項(xiàng)目mall(35k+star)地址:https://github.com/macrozheng/mall
前置知識(shí)
我們將采用Nacos作為注冊(cè)中心咖耘,Gateway作為網(wǎng)關(guān),使用
nimbus-jose-jwt
JWT庫操作JWT令牌撬码,對(duì)這些技術(shù)不了解的朋友可以看下下面的文章儿倒。
- Spring Cloud Gateway:新一代API網(wǎng)關(guān)服務(wù)
- Spring Cloud Alibaba:Nacos 作為注冊(cè)中心和配置中心使用
- 聽說你的JWT庫用起來特別扭,推薦這款賊好用的呜笑!
應(yīng)用架構(gòu)
我們理想的解決方案應(yīng)該是這樣的夫否,認(rèn)證服務(wù)負(fù)責(zé)認(rèn)證,網(wǎng)關(guān)負(fù)責(zé)校驗(yàn)認(rèn)證和鑒權(quán)叫胁,其他API服務(wù)負(fù)責(zé)處理自己的業(yè)務(wù)邏輯凰慈。安全相關(guān)的邏輯只存在于認(rèn)證服務(wù)和網(wǎng)關(guān)服務(wù)中,其他服務(wù)只是單純地提供服務(wù)而沒有任何安全相關(guān)邏輯驼鹅。
相關(guān)服務(wù)劃分:
- micro-oauth2-gateway:網(wǎng)關(guān)服務(wù)微谓,負(fù)責(zé)請(qǐng)求轉(zhuǎn)發(fā)和鑒權(quán)功能,整合Spring Security+Oauth2输钩;
- micro-oauth2-auth:Oauth2認(rèn)證服務(wù)豺型,負(fù)責(zé)對(duì)登錄用戶進(jìn)行認(rèn)證,整合Spring Security+Oauth2买乃;
- micro-oauth2-api:受保護(hù)的API服務(wù)姻氨,用戶鑒權(quán)通過后可以訪問該服務(wù),不整合Spring Security+Oauth2剪验。
方案實(shí)現(xiàn)
下面介紹下這套解決方案的具體實(shí)現(xiàn)肴焊,依次搭建認(rèn)證服務(wù)前联、網(wǎng)關(guān)服務(wù)和API服務(wù)。
micro-oauth2-auth
我們首先來搭建認(rèn)證服務(wù)抖韩,它將作為Oauth2的認(rèn)證服務(wù)使用蛀恩,并且網(wǎng)關(guān)服務(wù)的鑒權(quán)功能也需要依賴它。
- 在
pom.xml
中添加相關(guān)依賴茂浮,主要是Spring Security双谆、Oauth2、JWT席揽、Redis相關(guān)依賴顽馋;
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.16</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
- 在
application.yml
中添加相關(guān)配置,主要是Nacos和Redis相關(guān)配置幌羞;
server:
port: 9401
spring:
profiles:
active: dev
application:
name: micro-oauth2-auth
cloud:
nacos:
discovery:
server-addr: localhost:8848
jackson:
date-format: yyyy-MM-dd HH:mm:ss
redis:
database: 0
port: 6379
host: localhost
password:
management:
endpoints:
web:
exposure:
include: "*"
- 使用
keytool
生成RSA證書jwt.jks
寸谜,復(fù)制到resource
目錄下,在JDK的bin
目錄下使用如下命令即可属桦;
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
- 創(chuàng)建
UserServiceImpl
類實(shí)現(xiàn)Spring Security的UserDetailsService
接口熊痴,用于加載用戶信息;
/**
* 用戶管理業(yè)務(wù)類
* Created by macro on 2020/6/19.
*/
@Service
public class UserServiceImpl implements UserDetailsService {
private List<UserDTO> userList;
@Autowired
private PasswordEncoder passwordEncoder;
@PostConstruct
public void initData() {
String password = passwordEncoder.encode("123456");
userList = new ArrayList<>();
userList.add(new UserDTO(1L,"macro", password,1, CollUtil.toList("ADMIN")));
userList.add(new UserDTO(2L,"andy", password,1, CollUtil.toList("TEST")));
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<UserDTO> findUserList = userList.stream().filter(item -> item.getUsername().equals(username)).collect(Collectors.toList());
if (CollUtil.isEmpty(findUserList)) {
throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR);
}
SecurityUser securityUser = new SecurityUser(findUserList.get(0));
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;
}
}
- 添加認(rèn)證服務(wù)相關(guān)配置
Oauth2ServerConfig
聂宾,需要配置加載用戶信息的服務(wù)UserServiceImpl
及RSA的鑰匙對(duì)KeyPair
果善;
/**
* 認(rèn)證服務(wù)器配置
* Created by macro on 2020/6/19.
*/
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
private final PasswordEncoder passwordEncoder;
private final UserServiceImpl userDetailsService;
private final AuthenticationManager authenticationManager;
private final JwtTokenEnhancer jwtTokenEnhancer;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client-app")
.secret(passwordEncoder.encode("123456"))
.scopes("all")
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(86400);
}
@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的內(nèi)容增強(qiáng)器
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService) //配置加載用戶信息的服務(wù)
.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下的證書中獲取秘鑰對(duì)
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
}
}
- 如果你想往JWT中添加自定義信息的話,比如說
登錄用戶的ID
系谐,可以自己實(shí)現(xiàn)TokenEnhancer
接口巾陕;
/**
* JWT內(nèi)容增強(qiáng)器
* Created by macro on 2020/6/19.
*/
@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<>();
//把用戶ID設(shè)置到JWT中
info.put("id", securityUser.getId());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}
- 由于我們的網(wǎng)關(guān)服務(wù)需要RSA的公鑰來驗(yàn)證簽名是否合法,所以認(rèn)證服務(wù)需要有個(gè)接口把公鑰暴露出來纪他;
/**
* 獲取RSA公鑰接口
* Created by macro on 2020/6/19.
*/
@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();
}
}
- 不要忘了還需要配置Spring Security鄙煤,允許獲取公鑰接口的訪問;
/**
* SpringSecurity配置
* Created by macro on 2020/6/19.
*/
@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()
.anyRequest().authenticated();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
- 創(chuàng)建一個(gè)資源服務(wù)
ResourceServiceImpl
茶袒,初始化的時(shí)候把資源與角色匹配關(guān)系緩存到Redis中梯刚,方便網(wǎng)關(guān)服務(wù)進(jìn)行鑒權(quán)的時(shí)候獲取。
/**
* 資源與角色匹配關(guān)系管理業(yè)務(wù)類
* Created by macro on 2020/6/19.
*/
@Service
public class ResourceServiceImpl {
private Map<String, List<String>> resourceRolesMap;
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@PostConstruct
public void initData() {
resourceRolesMap = new TreeMap<>();
resourceRolesMap.put("/api/hello", CollUtil.toList("ADMIN"));
resourceRolesMap.put("/api/user/currentUser", CollUtil.toList("ADMIN", "TEST"));
redisTemplate.opsForHash().putAll(RedisConstant.RESOURCE_ROLES_MAP, resourceRolesMap);
}
}
micro-oauth2-gateway
接下來我們就可以搭建網(wǎng)關(guān)服務(wù)了薪寓,它將作為Oauth2的資源服務(wù)乾巧、客戶端服務(wù)使用,對(duì)訪問微服務(wù)的請(qǐng)求進(jìn)行統(tǒng)一的校驗(yàn)認(rèn)證和鑒權(quán)操作预愤。
- 在
pom.xml
中添加相關(guān)依賴沟于,主要是Gateway、Oauth2和JWT相關(guān)依賴植康;
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<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>8.16</version>
</dependency>
</dependencies>
- 在
application.yml
中添加相關(guān)配置旷太,主要是路由規(guī)則的配置、Oauth2中RSA公鑰的配置及路由白名單的配置;
server:
port: 9201
spring:
profiles:
active: dev
application:
name: micro-oauth2-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes: #配置路由規(guī)則
- id: oauth2-api-route
uri: lb://micro-oauth2-api
predicates:
- Path=/api/**
filters:
- StripPrefix=1
- id: oauth2-auth-route
uri: lb://micro-oauth2-auth
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
discovery:
locator:
enabled: true #開啟從注冊(cè)中心動(dòng)態(tài)創(chuàng)建路由的功能
lower-case-service-id: true #使用小寫服務(wù)名供璧,默認(rèn)是大寫
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: 'http://localhost:9401/rsa/publicKey' #配置RSA的公鑰訪問地址
redis:
database: 0
port: 6379
host: localhost
password:
secure:
ignore:
urls: #配置白名單路徑
- "/actuator/**"
- "/auth/oauth/token"
- 對(duì)網(wǎng)關(guān)服務(wù)進(jìn)行配置安全配置存崖,由于Gateway使用的是
WebFlux
,所以需要使用@EnableWebFluxSecurity
注解開啟睡毒;
/**
* 資源服務(wù)器配置
* Created by macro on 2020/6/19.
*/
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
private final AuthorizationManager authorizationManager;
private final IgnoreUrlsConfig ignoreUrlsConfig;
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)//鑒權(quán)管理器配置
.and().exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)//處理未授權(quán)
.authenticationEntryPoint(restAuthenticationEntryPoint)//處理未認(rèn)證
.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);
}
}
- 在
WebFluxSecurity
中自定義鑒權(quán)操作需要實(shí)現(xiàn)ReactiveAuthorizationManager
接口来惧;
/**
* 鑒權(quán)管理器,用于判斷是否有資源的訪問權(quán)限
* Created by macro on 2020/6/19.
*/
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
//從Redis中獲取當(dāng)前路徑可訪問角色列表
URI uri = authorizationContext.getExchange().getRequest().getURI();
Object obj = redisTemplate.opsForHash().get(RedisConstant.RESOURCE_ROLES_MAP, uri.getPath());
List<String> authorities = Convert.toList(String.class,obj);
authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList());
//認(rèn)證通過且角色匹配的用戶可訪問當(dāng)前路徑
return mono
.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
.any(authorities::contains)
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));
}
}
- 這里我們還需要實(shí)現(xiàn)一個(gè)全局過濾器
AuthGlobalFilter
演顾,當(dāng)鑒權(quán)通過后將JWT令牌中的用戶信息解析出來供搀,然后存入請(qǐng)求的Header中,這樣后續(xù)服務(wù)就不需要解析JWT令牌了钠至,可以直接從請(qǐng)求的Header中獲取到用戶信息葛虐。
/**
* 將登錄用戶的JWT轉(zhuǎn)化成用戶信息的全局過濾器
* Created by macro on 2020/6/17.
*/
@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("Authorization");
if (StrUtil.isEmpty(token)) {
return chain.filter(exchange);
}
try {
//從token中解析用戶信息并設(shè)置到Header中去
String realToken = token.replace("Bearer ", "");
JWSObject jwsObject = JWSObject.parse(realToken);
String userStr = jwsObject.getPayload().toString();
LOGGER.info("AuthGlobalFilter.filter() user:{}",userStr);
ServerHttpRequest request = exchange.getRequest().mutate().header("user", userStr).build();
exchange = exchange.mutate().request(request).build();
} catch (ParseException e) {
e.printStackTrace();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
micro-oauth2-api
最后我們搭建一個(gè)API服務(wù),它不會(huì)集成和實(shí)現(xiàn)任何安全相關(guān)邏輯棉钧,全靠網(wǎng)關(guān)來保護(hù)它屿脐。
- 在
pom.xml
中添加相關(guān)依賴,就添加了一個(gè)web依賴宪卿;
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
- 在
application.yml
添加相關(guān)配置的诵,很常規(guī)的配置;
server:
port: 9501
spring:
profiles:
active: dev
application:
name: micro-oauth2-api
cloud:
nacos:
discovery:
server-addr: localhost:8848
management:
endpoints:
web:
exposure:
include: "*"
- 創(chuàng)建一個(gè)測(cè)試接口佑钾,網(wǎng)關(guān)驗(yàn)證通過即可訪問西疤;
/**
* 測(cè)試接口
* Created by macro on 2020/6/19.
*/
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello World.";
}
}
- 創(chuàng)建一個(gè)
LoginUserHolder
組件,用于從請(qǐng)求的Header中直接獲取登錄用戶信息次绘;
/**
* 獲取登錄用戶信息
* Created by macro on 2020/6/17.
*/
@Component
public class LoginUserHolder {
public UserDTO getCurrentUser(){
//從Header中獲取用戶信息
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
String userStr = request.getHeader("user");
JSONObject userJsonObject = new JSONObject(userStr);
UserDTO userDTO = new UserDTO();
userDTO.setUsername(userJsonObject.getStr("user_name"));
userDTO.setId(Convert.toLong(userJsonObject.get("id")));
userDTO.setRoles(Convert.toList(String.class,userJsonObject.get("authorities")));
return userDTO;
}
}
- 創(chuàng)建一個(gè)獲取當(dāng)前用戶信息的接口瘪阁。
/**
* 獲取登錄用戶信息接口
* Created by macro on 2020/6/19.
*/
@RestController
@RequestMapping("/user")
public class UserController{
@Autowired
private LoginUserHolder loginUserHolder;
@GetMapping("/currentUser")
public UserDTO currentUser() {
return loginUserHolder.getCurrentUser();
}
}
功能演示
接下來我們來演示下微服務(wù)系統(tǒng)中的統(tǒng)一認(rèn)證鑒權(quán)功能撒遣,所有請(qǐng)求均通過網(wǎng)關(guān)訪問邮偎。
- 在此之前先啟動(dòng)我們的Nacos和Redis服務(wù),然后依次啟動(dòng)
micro-oauth2-auth
义黎、micro-oauth2-gateway
及micro-oauth2-api
服務(wù)禾进;
- 使用密碼模式獲取JWT令牌,訪問地址:http://localhost:9201/auth/oauth/token
- 使用獲取到的JWT令牌訪問需要權(quán)限的接口廉涕,訪問地址:http://localhost:9201/api/hello
- 使用獲取到的JWT令牌訪問獲取當(dāng)前登錄用戶信息的接口泻云,訪問地址:http://localhost:9201/api/user/currentUser
- 當(dāng)JWT令牌過期時(shí),使用refresh_token獲取新的JWT令牌狐蜕,訪問地址:http://localhost:9201/auth/oauth/token
- 使用沒有訪問權(quán)限的
andy
賬號(hào)登錄宠纯,訪問接口時(shí)會(huì)返回如下信息,訪問地址:http://localhost:9201/api/hello
項(xiàng)目源碼地址
https://github.com/macrozheng/springcloud-learning/tree/master/micro-oauth2
本文 GitHub https://github.com/macrozheng/mall-learning 已經(jīng)收錄层释,歡迎大家Star婆瓜!
作者:夢(mèng)想de星空
鏈接:http://www.reibang.com/p/1e974fc91f74