From b0a91e048743b76abbe66ce484908c4ea3c212d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johnny=20Miller=20=28=E9=94=BA=E4=BF=8A=29?= Date: Mon, 21 Dec 2020 15:25:04 +0800 Subject: [PATCH] feat($gateway): complete authentication flow --- .../WebSecurityConfiguration.java | 40 +++++++++++++++ .../AuthCenterApplicationTests.java | 32 +++++++++++- .../remoteapi/AuthCenterRemoteApi.java | 10 ++++ .../configuration/AuthenticationManager.java | 51 +++++++++++-------- ...CustomServerSecurityContextRepository.java | 31 +++++------ .../configuration/JwtServiceImpl.java | 2 +- .../RbacReactiveAuthorizationManager.java | 40 +++++++++++++++ .../ServerAuthenticationEntryPointImpl.java | 1 + .../SwaggerResourceProvider.java | 1 - .../configuration/UserPrincipal.java | 16 +++--- .../WebFluxSecurityConfiguration.java | 6 ++- 11 files changed, 177 insertions(+), 53 deletions(-) create mode 100644 auth-center/src/main/java/com/jmsoftware/maf/authcenter/universal/configuration/WebSecurityConfiguration.java create mode 100644 gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/RbacReactiveAuthorizationManager.java diff --git a/auth-center/src/main/java/com/jmsoftware/maf/authcenter/universal/configuration/WebSecurityConfiguration.java b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/universal/configuration/WebSecurityConfiguration.java new file mode 100644 index 00000000..810d07a6 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/maf/authcenter/universal/configuration/WebSecurityConfiguration.java @@ -0,0 +1,40 @@ +package com.jmsoftware.maf.authcenter.universal.configuration; + +import lombok.extern.slf4j.Slf4j; +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; + +/** + *

WebSecurityConfiguration

+ *

+ * Security handler configuration. + * + * @author Johnny Miller (锺俊), email: johnnysviva@outlook.com + * @date 5/2/20 11:41 PM + **/ +@Slf4j +@Configuration +@EnableWebSecurity +public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { + @Bean + public BCryptPasswordEncoder encoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + @Override + public AuthenticationManager authenticationManager() throws Exception { + return super.authenticationManagerBean(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + // Disable Web Security. + http.authorizeRequests().anyRequest().permitAll().and().csrf().disable(); + } +} diff --git a/auth-center/src/test/java/com/jmsoftware/maf/authcenter/AuthCenterApplicationTests.java b/auth-center/src/test/java/com/jmsoftware/maf/authcenter/AuthCenterApplicationTests.java index 0b998518..c9f39213 100644 --- a/auth-center/src/test/java/com/jmsoftware/maf/authcenter/AuthCenterApplicationTests.java +++ b/auth-center/src/test/java/com/jmsoftware/maf/authcenter/AuthCenterApplicationTests.java @@ -1,13 +1,41 @@ package com.jmsoftware.maf.authcenter; +import cn.hutool.core.util.StrUtil; +import com.jmsoftware.maf.authcenter.universal.domain.UserPO; +import com.jmsoftware.maf.authcenter.universal.domain.UserPrincipal; +import com.jmsoftware.maf.authcenter.universal.service.JwtService; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import java.util.ArrayList; + +/** + * Description: AuthCenterApplicationTests. + * + * @author 钟俊 (zhongjun), email: zhongjun@toguide.cn, date: 12/21/2020 3:08 PM + */ +@Slf4j @SpringBootTest class AuthCenterApplicationTests { + @Autowired + private JwtService jwtService; @Test - void contextLoads() { + @SneakyThrows + void mockLogin() { + UserPO userPO = new UserPO(); + userPO.setId(1L); + userPO.setUsername("ijohnnymiller"); + val authenticationToken = new UsernamePasswordAuthenticationToken( + UserPrincipal.create(userPO, new ArrayList<>(), new ArrayList<>()), 12345678); + String jwt = jwtService.createJwt(authenticationToken, false); + log.info("Generated JWT: {}", jwt); + Assertions.assertTrue(StrUtil.isNotBlank(jwt)); } - } diff --git a/gateway/src/main/java/com/jmsoftware/maf/gateway/remoteapi/AuthCenterRemoteApi.java b/gateway/src/main/java/com/jmsoftware/maf/gateway/remoteapi/AuthCenterRemoteApi.java index ce296e2c..c5a095a7 100644 --- a/gateway/src/main/java/com/jmsoftware/maf/gateway/remoteapi/AuthCenterRemoteApi.java +++ b/gateway/src/main/java/com/jmsoftware/maf/gateway/remoteapi/AuthCenterRemoteApi.java @@ -2,6 +2,7 @@ import com.jmsoftware.maf.common.bean.ResponseBodyBean; import com.jmsoftware.maf.common.domain.authcenter.role.GetRoleListByUserIdResponse; +import com.jmsoftware.maf.common.domain.authcenter.user.GetUserByLoginTokenResponse; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -19,6 +20,15 @@ @Validated @ReactiveFeignClient(name = "auth-center") public interface AuthCenterRemoteApi { + /** + * Gets user by login token. + * + * @param loginToken the login token, e.q. username, email or phone number + * @return the user by login token + */ + @GetMapping("/user-remote-api/users/{loginToken}") + Mono> getUserByLoginToken(@PathVariable String loginToken); + /** * Gets role list by user id. * diff --git a/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/AuthenticationManager.java b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/AuthenticationManager.java index 16226d0d..8622c3f3 100644 --- a/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/AuthenticationManager.java +++ b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/AuthenticationManager.java @@ -1,50 +1,57 @@ package com.jmsoftware.maf.gateway.universal.configuration; -import com.google.common.collect.Lists; +import cn.hutool.core.util.StrUtil; +import com.jmsoftware.maf.common.bean.ResponseBodyBean; +import com.jmsoftware.maf.common.domain.authcenter.user.GetUserByLoginTokenResponse; +import com.jmsoftware.maf.common.exception.BusinessException; +import com.jmsoftware.maf.gateway.remoteapi.AuthCenterRemoteApi; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.context.annotation.Lazy; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; -import java.util.List; +import javax.annotation.Resource; /** * Description: AuthenticationManager, change description here. * * @author 钟俊(zhongjun), email: zhongjun@toguide.cn, date: 12/18/2020 3:40 PM **/ +@Slf4j @Component @RequiredArgsConstructor public class AuthenticationManager implements ReactiveAuthenticationManager { private final JwtService jwtService; + @Lazy + @Resource + private AuthCenterRemoteApi authCenterRemoteApi; @Override public Mono authenticate(Authentication authentication) { - String authToken = authentication.getCredentials().toString(); + val jwt = authentication.getCredentials().toString(); String username; try { - username = jwtService.getUsernameFromJwt(authToken); + username = jwtService.getUsernameFromJwt(jwt); } catch (Exception e) { - username = null; + log.error("Exception occurred when authenticating", e); + return Mono.empty(); } -// if (username != null && !tokenProvider.isTokenExpired(authToken)) { -// Claims claims = tokenProvider.getAllClaimsFromToken(authToken); -// List roles = claims.get(AUTHORITIES_KEY, List.class); -// List authorities = roles.stream().map(role -> new SimpleGrantedAuthority(role)).collect( -// Collectors.toList()); -// UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, username, -// authorities); -// SecurityContextHolder.getContext().setAuthentication(new UserPrincipal(username, authorities)); -// return Mono.just(auth); -// } else { -// return Mono.empty(); -// } - List authorities = Lists.newLinkedList(); - UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, username, - authorities); - return Mono.just(auth); + if (StrUtil.isBlank(username)) { + log.warn("Authentication failed! Cause: the username mustn't be blank"); + return Mono.empty(); + } + val response = authCenterRemoteApi.getUserByLoginToken(username); + Mono responseMono = response.map(ResponseBodyBean::getData) + .switchIfEmpty(Mono.error(new BusinessException("Authentication failed! Cause: User not found"))); + return responseMono.map(getUserByLoginTokenResponse -> { + log.info("Authentication success. Username: {}", getUserByLoginTokenResponse.getUsername()); + UserPrincipal userPrincipal = UserPrincipal.create(getUserByLoginTokenResponse, null, null); + return new UsernamePasswordAuthenticationToken(userPrincipal, null); + }); } } diff --git a/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/CustomServerSecurityContextRepository.java b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/CustomServerSecurityContextRepository.java index 114381d2..32329c91 100644 --- a/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/CustomServerSecurityContextRepository.java +++ b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/CustomServerSecurityContextRepository.java @@ -1,7 +1,8 @@ package com.jmsoftware.maf.gateway.universal.configuration; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.security.authentication.ReactiveAuthenticationManager; @@ -21,31 +22,27 @@ **/ @Slf4j @Component +@RequiredArgsConstructor public class CustomServerSecurityContextRepository implements ServerSecurityContextRepository { private static final String TOKEN_PREFIX = "Bearer "; - - @Autowired - private ReactiveAuthenticationManager authenticationManager; + private final ReactiveAuthenticationManager authenticationManager; @Override - public Mono save(ServerWebExchange swe, SecurityContext sc) { + public Mono save(ServerWebExchange exchange, SecurityContext context) { throw new UnsupportedOperationException("Not supported yet."); } @Override - public Mono load(ServerWebExchange swe) { - ServerHttpRequest request = swe.getRequest(); - String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); - String authToken = null; - if (authHeader != null && authHeader.startsWith(TOKEN_PREFIX)) { - authToken = authHeader.replace(TOKEN_PREFIX, ""); - } - if (authToken != null) { - Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken); - return this.authenticationManager.authenticate(auth).map(SecurityContextImpl::new); - } else { + public Mono load(ServerWebExchange exchange) { + ServerHttpRequest request = exchange.getRequest(); + String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); + if (StrUtil.isBlank(authorization) || !authorization.startsWith(TOKEN_PREFIX)) { + log.warn("{} in HTTP headers not found! [{}] {}", HttpHeaders.AUTHORIZATION, request.getMethod(), + request.getURI()); return Mono.empty(); } + String jwt = authorization.replace(TOKEN_PREFIX, ""); + Authentication authentication = new UsernamePasswordAuthenticationToken(null, jwt); + return this.authenticationManager.authenticate(authentication).map(SecurityContextImpl::new); } - } diff --git a/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/JwtServiceImpl.java b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/JwtServiceImpl.java index 0b9644a6..161d875c 100644 --- a/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/JwtServiceImpl.java +++ b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/JwtServiceImpl.java @@ -91,7 +91,7 @@ public Claims parseJwt(String jwt) throws SecurityException { .orElseThrow(() -> new SecurityException(HttpStatus.TOKEN_PARSE_ERROR, "The JWT Claims Set is null", null)); } catch (ExpiredJwtException e) { - log.error("JWT is expired. Message: {} JWT: {}", e.getMessage(), jwt); + log.error("JWT was expired. Message: {} JWT: {}", e.getMessage(), jwt); throw new SecurityException(HttpStatus.TOKEN_EXPIRED); } catch (UnsupportedJwtException e) { log.error("JWT is unsupported. Message: {} JWT: {}", e.getMessage(), jwt); diff --git a/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/RbacReactiveAuthorizationManager.java b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/RbacReactiveAuthorizationManager.java new file mode 100644 index 00000000..2e3cc81e --- /dev/null +++ b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/RbacReactiveAuthorizationManager.java @@ -0,0 +1,40 @@ +package com.jmsoftware.maf.gateway.universal.configuration; + +import com.jmsoftware.maf.gateway.remoteapi.AuthCenterRemoteApi; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +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.web.server.authorization.AuthorizationContext; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import javax.annotation.Resource; + +/** + * Description: ReactiveAuthorizationManagerImpl, change description here. + * + * @author 钟俊(zhongjun), email: zhongjun@toguide.cn, date: 12/21/2020 12:38 PM + **/ +@Slf4j +@Component +@RequiredArgsConstructor +public class RbacReactiveAuthorizationManager implements ReactiveAuthorizationManager { + @Lazy + @Resource + private AuthCenterRemoteApi authCenterRemoteApi; + + @Override + public Mono check(Mono authentication, AuthorizationContext object) { + return authentication.map(auth -> { + ServerHttpRequest request = object.getExchange().getRequest(); + UserPrincipal userPrincipal = (UserPrincipal) auth.getPrincipal(); + log.info("Checking authorization for user: {}, resource: [{}] {}", userPrincipal.getUsername(), + request.getMethod(), request.getURI()); + return new AuthorizationDecision(true); + }).defaultIfEmpty(new AuthorizationDecision(false)); + } +} diff --git a/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/ServerAuthenticationEntryPointImpl.java b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/ServerAuthenticationEntryPointImpl.java index e99887e6..50fd47c7 100644 --- a/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/ServerAuthenticationEntryPointImpl.java +++ b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/ServerAuthenticationEntryPointImpl.java @@ -21,6 +21,7 @@ public class ServerAuthenticationEntryPointImpl implements ServerAuthenticationE public Mono commence(ServerWebExchange serverWebExchange, AuthenticationException e) { log.error("Exception occurred when authenticating! Exception message: {}. Request URL: [{}] {}", e.getMessage(), serverWebExchange.getRequest().getMethod(), serverWebExchange.getRequest().getURI()); + log.error("{}", e.getMessage(), e); return ResponseUtil.renderJson(serverWebExchange, HttpStatus.NETWORK_AUTHENTICATION_REQUIRED, null); } } diff --git a/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/SwaggerResourceProvider.java b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/SwaggerResourceProvider.java index 39d22e65..fb4be27a 100644 --- a/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/SwaggerResourceProvider.java +++ b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/SwaggerResourceProvider.java @@ -3,7 +3,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import lombok.val; -import org.springframework.cloud.gateway.config.GatewayProperties; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; diff --git a/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/UserPrincipal.java b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/UserPrincipal.java index 3c9f6a87..96d879ca 100644 --- a/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/UserPrincipal.java +++ b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/UserPrincipal.java @@ -13,10 +13,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.stream.Collectors; /** @@ -96,11 +93,12 @@ public class UserPrincipal implements UserDetails { */ public static UserPrincipal create(GetUserByLoginTokenResponse user, List roleNameList, List permissionList) { - val authorities = - permissionList.stream() - .filter(permission -> StrUtil.isNotBlank(permission.getPermissionExpression())) - .map(permission -> new SimpleGrantedAuthority(permission.getPermissionExpression())) - .collect(Collectors.toList()); + val permissions = + Optional.ofNullable(permissionList).orElse(new LinkedList<>()); + val authorities = permissions.stream() + .filter(permission -> StrUtil.isNotBlank(permission.getPermissionExpression())) + .map(permission -> new SimpleGrantedAuthority(permission.getPermissionExpression())) + .collect(Collectors.toList()); return new UserPrincipal(user.getId(), user.getUsername(), diff --git a/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/WebFluxSecurityConfiguration.java b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/WebFluxSecurityConfiguration.java index e13f77b8..906f957a 100644 --- a/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/WebFluxSecurityConfiguration.java +++ b/gateway/src/main/java/com/jmsoftware/maf/gateway/universal/configuration/WebFluxSecurityConfiguration.java @@ -34,6 +34,7 @@ public class WebFluxSecurityConfiguration { private final CustomConfiguration customConfiguration; private final ReactiveAuthenticationManager reactiveAuthenticationManager; + private final RbacReactiveAuthorizationManager reactiveAuthorizationManager; private final ServerSecurityContextRepository securityContextRepository; private final ServerAuthenticationEntryPointImpl serverAuthenticationEntryPointImpl; private final CustomServerAccessDeniedHandler customServerAccessDeniedHandler; @@ -49,12 +50,15 @@ SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) { .accessDeniedHandler(customServerAccessDeniedHandler) .and() .addFilterBefore(requestFilter, SecurityWebFiltersOrder.AUTHENTICATION) + // Authentication .authenticationManager(reactiveAuthenticationManager) .securityContextRepository(securityContextRepository) .authorizeExchange() .pathMatchers(flattenIgnoredUrls()).permitAll() .pathMatchers(HttpMethod.OPTIONS).permitAll() - .anyExchange().authenticated() +// .anyExchange().authenticated() + // Authorization + .anyExchange().access(reactiveAuthorizationManager) .and() .build(); }