Mall (35K + STAR) Address: github.com/macrozheng/…
Abstract
Recently, a good microservice permission solution has been found, which allows unified authentication through the authentication service, and then unified authentication and authentication through the gateway. This scheme is the latest scheme at present, only support Spring Boot 2.2.0, Spring Cloud Hoxton version above, this article will introduce the implementation of the scheme in detail, I hope to help you!
Front knowledge
We will use Nacos as the registry, Gateway as the Gateway, and nimbus-jos-JWtJWT library to manipulate JWT tokens. For those who are not familiar with these technologies, see the following article.
- Spring Cloud Gateway: a next-generation API Gateway service
- Spring Cloud Alibaba: Nacos is used as the registry and configuration center
- I heard that your JWT library is particularly twisty to use, recommend this thief easy to use!
Application architecture
Our ideal solution would be for authentication services to authenticate, gateways to verify authentication and authentication, and other API services to handle their own business logic. Security-related logic exists only in authentication services and gateway services. Other services simply provide services without any security-related logic.
Related service division:
- Micro-oauth2-gateway: Gateway service, responsible for request forwarding and authentication, integrating Spring Security+ OAuth2;
- Micro-oauth2-auth: OAuth2 authentication service, responsible for authenticating login users, integrating Spring Security+ OAuth2;
- Micro-oauth2-api: a protected API service. Users can access the service after passing authentication. Spring Security and OAuth2 are not integrated.
Plan implementation
The concrete implementation of this solution is described below, and the authentication service, gateway service and API service are set up in sequence.
micro-oauth2-auth
We’ll start by setting up the authentication service, which will be used as Oauth2’s authentication service and on which the gateway service’s authentication function will depend.
- in
pom.xml
Add related dependencies, mainly Spring Security, Oauth2, JWT, Redis related dependencies;
<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>
Copy the code
- in
application.yml
To add related configurations, mainly Nacos and Redis related configurations;
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: "*"
Copy the code
- use
keytool
Generating an RSA Certificatejwt.jks
And copied to theresource
Directory in the JDKbin
Run the following command in the directory.
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
Copy the code
- create
UserServiceImpl
Class that implements Spring SecurityUserDetailsService
Interface for loading user information.
/** * 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(a) {
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);
}
returnsecurityUser; }}Copy the code
- Add authentication service configuration
Oauth2ServerConfig
You need to configure the service for loading user informationUserServiceImpl
And RSA key pairKeyPair
;
/** * Authentication server configuration * 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); // Configure the JWT content enhancer
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService) // Configure the service for loading user information
.accessTokenConverter(accessTokenConverter())
.tokenEnhancer(enhancerChain);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}
@Bean
public JwtAccessTokenConverter accessTokenConverter(a) {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setKeyPair(keyPair());
return jwtAccessTokenConverter;
}
@Bean
public KeyPair keyPair(a) {
// Obtain the secret key pair from the classpath certificate
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
return keyStoreKeyFactory.getKeyPair("jwt"."123456".toCharArray()); }}Copy the code
- If you want to add custom information to JWT, for example
ID of the login user
You can do it yourselfTokenEnhancer
Interface;
/** * JWT content enhancer * 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<>();
// Set the user ID to JWT
info.put("id", securityUser.getId());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
returnaccessToken; }}Copy the code
- Since our gateway service requires the RSA public key to verify the signature, the authentication service needs an interface to expose the public key.
/** * Get RSA public key interface * Created by Macro on 2020/6/19. */
@RestController
public class KeyPairController {
@Autowired
private KeyPair keyPair;
@GetMapping("/rsa/publicKey")
public Map<String, Object> getKey(a) {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAKey key = new RSAKey.Builder(publicKey).build();
return newJWKSet(key).toJSONObject(); }}Copy the code
- Don’t forget to also configure Spring Security to allow access to the public key interface;
/** * SpringSecurity configuration * 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(a) throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder(a) {
return newBCryptPasswordEncoder(); }}Copy the code
- Create a resource service
ResourceServiceImpl
During initialization, the matching relationship between resources and roles is cached in Redis for the gateway service to obtain during authentication.
/** * 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(a) {
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); }}Copy the code
micro-oauth2-gateway
Next we can set up the gateway service, which will be used as Oauth2 resource service, client service, to access the microservice requests for unified authentication and authentication operations.
- in
pom.xml
Add related dependencies, mainly Gateway, Oauth2 and JWT related dependencies;
<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>
Copy the code
- in
application.yml
, including the configuration of routing rules, Oauth2 RSA public key, and routing whitelist.
server:
port: 9201
spring:
profiles:
active: dev
application:
name: micro-oauth2-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes: # Configure routing rules
- 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 Enable dynamic route creation from the registry
lower-case-service-id: true Use lowercase service name, default is uppercase
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: 'http://localhost:9401/rsa/publicKey' Configure the RSA public key access address
redis:
database: 0
port: 6379
host: localhost
password:
secure:
ignore:
urls: Configure the whitelist path
- "/actuator/**"
- "/auth/oauth/token"
Copy the code
- Configure security configuration for the Gateway service because the Gateway uses
WebFlux
, so you need to use@EnableWebFluxSecurity
Annotation open;
/** * Resource server configuration * 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()// Configure the whitelist
.anyExchange().access(authorizationManager)// Authentication manager configuration
.and().exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)// Handle unauthorized
.authenticationEntryPoint(restAuthenticationEntryPoint)// Handle non-authentication
.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 newReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); }}Copy the code
- in
WebFluxSecurity
You need to implement the user-defined authentication operationsReactiveAuthorizationManager
Interface;
/** * Id manager, which is used to determine whether there is access to a resource * 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) {
// Obtain the list of roles accessible to the current path from Redis
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());
// The user who passes the authentication and matches the role can access the current path
return mono
.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
.any(authorities::contains)
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false)); }}Copy the code
- We also need to implement a global filter here
AuthGlobalFilter
After the authentication is passed, the user information in the JWT token is parsed and stored in the requested Header. In this way, the subsequent service does not need to parse the JWT token and can directly obtain the user information from the requested Header.
/** * Create 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 {
// Parse the user information from the token and set it to the 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(a) {
return 0; }}Copy the code
micro-oauth2-api
Finally, we set up an API service that does not integrate and implement any security-related logic, and relies solely on the gateway to protect it.
- in
pom.xml
You add a Web dependency by adding a related dependency to the
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
Copy the code
- in
application.yml
Add the relevant configuration, very general configuration;
server:
port: 9501
spring:
profiles:
active: dev
application:
name: micro-oauth2-api
cloud:
nacos:
discovery:
server-addr: localhost:8848
management:
endpoints:
web:
exposure:
include: "*"
Copy the code
- Create a test interface, gateway authentication can access;
/** * Test interface * Created by Macro on 2020/6/19
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(a) {
return "Hello World."; }}Copy the code
- To create a
LoginUserHolder
Component for retrieving login user information directly from the request Header
/** * Create by Macro on 2020/6/17. */
@Component
public class LoginUserHolder {
public UserDTO getCurrentUser(a){
// Get the user information from the 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")));
returnuserDTO; }}Copy the code
- Create an interface to get information about the current user.
/** * Get user information * Created by Macro on 2020/6/19. */
@RestController
@RequestMapping("/user")
public class UserController{
@Autowired
private LoginUserHolder loginUserHolder;
@GetMapping("/currentUser")
public UserDTO currentUser(a) {
returnloginUserHolder.getCurrentUser(); }}Copy the code
Function demonstration
Next, we will demonstrate the unified authentication function in the microservice system. All requests are accessed through the gateway.
- Start our Nacos and Redis services first, and then in turn
micro-oauth2-auth
,micro-oauth2-gateway
andmicro-oauth2-api
Service;
- Use password mode for JWT token, access to the address: http://localhost:9201/auth/oauth/token
- Need to use access to JWT token access interfaces, access address: http://localhost:9201/api/hello
- Use access to JWT token access to retrieve information currently logged in user interface, access address: http://localhost:9201/api/user/currentUser
- When the JWT token expired, use refresh_token for new JWT token, access to the address: http://localhost:9201/auth/oauth/token
- Use one that has no access permission
andy
When an account is used to log in and access the interface, the following information is returned:http://localhost:9201/api/hello
Project source address
Github.com/macrozheng/…
The public,
Mall project full set of learning tutorials serialized, pay attention to the public account for the first time.