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.

  • inpom.xmlAdd 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
  • inapplication.ymlTo 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
  • usekeytoolGenerating an RSA Certificatejwt.jksAnd copied to theresourceDirectory in the JDKbinRun the following command in the directory.
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
Copy the code
  • createUserServiceImplClass that implements Spring SecurityUserDetailsServiceInterface 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 configurationOauth2ServerConfigYou need to configure the service for loading user informationUserServiceImplAnd 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 exampleID of the login userYou can do it yourselfTokenEnhancerInterface;
/** * 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 serviceResourceServiceImplDuring 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.

  • inpom.xmlAdd 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
  • inapplication.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 usesWebFlux, so you need to use@EnableWebFluxSecurityAnnotation 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
  • inWebFluxSecurityYou need to implement the user-defined authentication operationsReactiveAuthorizationManagerInterface;
/** * 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 hereAuthGlobalFilterAfter 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.

  • inpom.xmlYou 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
  • inapplication.ymlAdd 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 aLoginUserHolderComponent 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 turnmicro-oauth2-auth,micro-oauth2-gatewayandmicro-oauth2-apiService;

  • 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 permissionandyWhen 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.