Spring Boot的 使用JWT進行認(rèn)證

使用JWT進行身份驗證

??應(yīng)用程序的Github存儲庫:https//github.com/OmarElGabry/microservices-spring-boot


身份驗證工作流程

身份驗證流程很簡單:

  1. 用戶發(fā)送請求以獲取傳遞其憑據(jù)的令牌(token)。
  2. 服務(wù)器驗證憑據(jù)并發(fā)回令牌。
  3. 對于每個請求,用戶必須提供令牌,服務(wù)器將驗證該令牌。

我們將引入另一項稱為“auth service”的服務(wù),用于驗證用戶憑據(jù)和頒發(fā)令牌。

驗證令牌怎么樣?好吧,它可以在auth service本身中實現(xiàn),并且網(wǎng)關(guān)必須在允許請求轉(zhuǎn)到任何服務(wù)之前調(diào)用auth service來驗證令牌。

相反,我們可以在網(wǎng)關(guān)級別驗證令牌,并讓auth service驗證用戶憑據(jù),并發(fā)出令牌。這就是我們要在這里做的事情。

在這兩種方式中,我們都會阻止請求,除非它經(jīng)過身份驗證(生成令牌的請求除外)。

基于JSON的令牌(JWT)

令牌是一個編碼字符串,由我們的應(yīng)用程序生成(經(jīng)過身份驗證后),并由用戶沿每個請求發(fā)送,以允許訪問我們的應(yīng)用程序公開的資源。

基于JSON的令牌(JWT)是一種基于JSON的開放標(biāo)準(zhǔn),用于創(chuàng)建訪問令牌。它由三部分組成; 標(biāo)頭(header),有效負(fù)載(payload)和簽名(signature)。

標(biāo)頭包含散列算法

{type: “JWT”, hash: “HS256”}

有效負(fù)載包含屬性(用戶名,電子郵件等)及其值。

{username: "Omar", email: "omar@example.com", admin: true }

簽名是哈希: Header + “.” + Payload + Secret key

網(wǎng)關(guān)

在網(wǎng)關(guān)中,我們需要做兩件事:(1)針對每個請求驗證令牌,以及(2)阻止對我們的服務(wù)的所有未經(jīng)身份驗證的請求。

pom.xml添加spring安全性和JWT依賴項中。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.0</version>
</dependency>

application.properties添加路徑AUTH服務(wù)(我們將在稍后創(chuàng)建它)。

# Map path to auth service
zuul.routes.auth-service.path=/auth/**
zuul.routes.auth-service.service-id=AUTH-SERVICE

# By default, all requests to gallery service for example will start with: "/gallery/"
# What will be sent to the gallery service is what comes after the path defined, 
# So, if request is "/gallery/view/1", gallery service will get "/view/1".
# In case of auth, we need to pass the "/auth/" in the path to auth service. So, set strip-prefix to false
zuul.routes.auth-service.strip-prefix=false

# Exclude authorization from sensitive headers
zuul.routes.auth-service.sensitive-headers=Cookie,Set-Cookie   

要定義我們的安全性配置,創(chuàng)建一個類,并使用注釋@EnableWebSecurity,并使用extends WebSecurityConfigurerAdapter類來覆蓋并提供我們自己的自定義安全性配置。

package com.eureka.zuul.security;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
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.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.eureka.zuul.security.JwtConfig;

@EnableWebSecurity  // Enable security config. This annotation denotes config for spring security.
public class SecurityTokenConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtConfig jwtConfig;
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
           http
        .csrf().disable()
            // make sure we use stateless session; session won't be used to store user's state.
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)     
        .and()
            // handle an authorized attempts 
            .exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))  
        .and()
           // Add a filter to validate the tokens with every request
           .addFilterAfter(new JwtTokenAuthenticationFilter(jwtConfig), UsernamePasswordAuthenticationFilter.class)
        // authorization requests config
        .authorizeRequests()
           // allow all who are accessing "auth" service
           .antMatchers(HttpMethod.POST, jwtConfig.getUri()).permitAll()  
           // must be an admin if trying to access admin area (authentication is also required here)
           .antMatchers("/gallery" + "/admin/**").hasRole("ADMIN")
           // Any other request must be authenticated
           .anyRequest().authenticated(); 
    }
    
    @Bean
    public JwtConfig jwtConfig() {
           return new JwtConfig();
    }
}

Spring具有將在請求的生命周期(過濾器鏈)內(nèi)執(zhí)行的過濾器。要啟用和使用這些過濾器,我們需要擴展任何這些過濾器的類。

默認(rèn)情況下,spring會嘗試確定何時應(yīng)該執(zhí)行過濾器。否則,我們還可以定義何時應(yīng)該執(zhí)行(在另一個過濾器之后或之前)。

JwtConfig只是一個包含配置變量的類。

public class JwtConfig {
    @Value("${security.jwt.uri:/auth/**}")
    private String Uri;

    @Value("${security.jwt.header:Authorization}")
    private String header;

    @Value("${security.jwt.prefix:Bearer }")
    private String prefix;

    @Value("${security.jwt.expiration:#{24*60*60}}")
    private int expiration;

    @Value("${security.jwt.secret:JwtSecretKey}")
    private String secret;
    
    // getters ...
}

最后一步是實現(xiàn)驗證令牌的過濾器。我們正在使用OncePerRequestFilter。它保證每個請求單次執(zhí)行(因為您可以在過濾器鏈上多次使用過濾器)。

package com.eureka.zuul.security;

import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import com.eureka.zuul.security.JwtConfig;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;

public class JwtTokenAuthenticationFilter extends  OncePerRequestFilter {
    
    private final JwtConfig jwtConfig;
    
    public JwtTokenAuthenticationFilter(JwtConfig jwtConfig) {
        this.jwtConfig = jwtConfig;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        
        // 1. get the authentication header. Tokens are supposed to be passed in the authentication header
        String header = request.getHeader(jwtConfig.getHeader());
        
        // 2. validate the header and check the prefix
        if(header == null || !header.startsWith(jwtConfig.getPrefix())) {
            chain.doFilter(request, response);          // If not valid, go to the next filter.
            return;
        }
        
        // If there is no token provided and hence the user won't be authenticated. 
        // It's Ok. Maybe the user accessing a public path or asking for a token.
        
        // All secured paths that needs a token are already defined and secured in config class.
        // And If user tried to access without access token, then he won't be authenticated and an exception will be thrown.
        
        // 3. Get the token
        String token = header.replace(jwtConfig.getPrefix(), "");
        
        try {   // exceptions might be thrown in creating the claims if for example the token is expired
            
            // 4. Validate the token
            Claims claims = Jwts.parser()
                    .setSigningKey(jwtConfig.getSecret().getBytes())
                    .parseClaimsJws(token)
                    .getBody();
            
            String username = claims.getSubject();
            if(username != null) {
                @SuppressWarnings("unchecked")
                List<String> authorities = (List<String>) claims.get("authorities");
                
                // 5. Create auth object
                // UsernamePasswordAuthenticationToken: A built-in object, used by spring to represent the current authenticated / being authenticated user.
                // It needs a list of authorities, which has type of GrantedAuthority interface, where SimpleGrantedAuthority is an implementation of that interface
                 UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                                 username, null, authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
                 
                 // 6. Authenticate the user
                 // Now, user is authenticated
                 SecurityContextHolder.getContext().setAuthentication(auth);
            }
            
        } catch (Exception e) {
            // In case of failure. Make sure it's clear; so guarantee user won't be authenticated
            SecurityContextHolder.clearContext();
        }
        
        // go to the next filter in the filter chain
        chain.doFilter(request, response);
    }

}

驗證服務(wù)(Auth Service)

在Auth Service中,我們需要(1)驗證用戶憑證,如果有效,(2)生成令牌,否則拋出異常。

pom.xml添加以下依賴項:Web,Eureka Client,Spring Security和JWT。

....
 <dependencies>
     <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
     </dependency>
     <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
     </dependency>
     <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <optional>true</optional>
     </dependency>
</dependencies>
....

在里面 application.properties

spring.application.name=auth-service
server.port=9100
eureka.client.service-url.default-zone=http://localhost:8761/eureka

正如我們在Gateway中進行安全配置所做的那樣,創(chuàng)建一個帶有注釋@EnableWebSecurity和擴展的類WebSecurityConfigurerAdapter

package com.eureka.auth.security;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import com.eureka.auth.security.JwtConfig;

@EnableWebSecurity  // Enable security config. This annotation denotes config for spring security.
public class SecurityCredentialsConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtConfig jwtConfig;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
             // make sure we use stateless session; session won't be used to store user's state.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
                // handle an authorized attempts 
                .exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))
            .and()
            // Add a filter to validate user credentials and add token in the response header
            
            // What's the authenticationManager()? 
            // An object provided by WebSecurityConfigurerAdapter, used to authenticate the user passing user's credentials
            // The filter needs this auth manager to authenticate the user.
            .addFilter(new JwtUsernameAndPasswordAuthenticationFilter(authenticationManager(), jwtConfig))  
        .authorizeRequests()
            // allow all POST requests 
            .antMatchers(HttpMethod.POST, jwtConfig.getUri()).permitAll()
            // any other requests must be authenticated
            .anyRequest().authenticated();
    }
    
    // Spring has UserDetailsService interface, which can be overriden to provide our implementation for fetching user from database (or any other source).
    // The UserDetailsService object is used by the auth manager to load the user from database.
    // In addition, we need to define the password encoder also. So, auth manager can compare and verify passwords.
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
    
    @Bean
    public JwtConfig jwtConfig() {
            return new JwtConfig();
    }
    
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

正如您在上面的代碼中看到的,我們需要實現(xiàn)UserDetailsService接口。

該類充當(dāng)用戶的提供者; 意味著它從數(shù)據(jù)庫(或任何數(shù)據(jù)源)加載用戶。它不進行身份驗證。它只是加載用戶的用戶名。

package com.eureka.auth.security;

import java.util.Arrays;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service   // It has to be annotated with @Service.
public class UserDetailsServiceImpl implements UserDetailsService  {
    
    @Autowired
    private BCryptPasswordEncoder encoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        
        // hard coding the users. All passwords must be encoded.
        final List<AppUser> users = Arrays.asList(
            new AppUser(1, "omar", encoder.encode("12345"), "USER"),
            new AppUser(2, "admin", encoder.encode("12345"), "ADMIN")
        );
        

        for(AppUser appUser: users) {
            if(appUser.getUsername().equals(username)) {
                
                // Remember that Spring needs roles to be in this format: "ROLE_" + userRole (i.e. "ROLE_ADMIN")
                // So, we need to set it to that format, so we can verify and compare roles (i.e. hasRole("ADMIN")).
                List<GrantedAuthority> grantedAuthorities = AuthorityUtils
                            .commaSeparatedStringToAuthorityList("ROLE_" + appUser.getRole());
                
                // The "User" class is provided by Spring and represents a model class for user to be returned by UserDetailsService
                // And used by auth manager to verify and check user authentication.
                return new User(appUser.getUsername(), appUser.getPassword(), grantedAuthorities);
            }
        }
        
        // If user not found. Throw this exception.
        throw new UsernameNotFoundException("Username: " + username + " not found");
    }
    
    // A (temporary) class represent the user saved in the database.
    private static class AppUser {
        private Integer id;
            private String username, password;
            private String role;
        
        public AppUser(Integer id, String username, String password, String role) {
                this.id = id;
                this.username = username;
                this.password = password;
                this.role = role;
            }

        // getters and setters ....
    }
}

這是最后一步; 過濾器。

我們正在使用JwtUsernameAndPasswordAuthenticationFilter。它用于驗證用戶憑據(jù)并生成令牌。必須在POST請求中發(fā)送用戶名和密碼。

package com.eureka.auth.security;

import java.io.IOException;
import java.sql.Date;
import java.util.Collections;
import java.util.stream.Collectors;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.eureka.auth.security.JwtConfig;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

public class JwtUsernameAndPasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter   {
    
    // We use auth manager to validate the user credentials
    private AuthenticationManager authManager;
    
    private final JwtConfig jwtConfig;
    
    public JwtUsernameAndPasswordAuthenticationFilter(AuthenticationManager authManager, JwtConfig jwtConfig) {
        this.authManager = authManager;
        this.jwtConfig = jwtConfig;
        
        // By default, UsernamePasswordAuthenticationFilter listens to "/login" path. 
        // In our case, we use "/auth". So, we need to override the defaults.
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(jwtConfig.getUri(), "POST"));
    }
    
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        
        try {
            
            // 1. Get credentials from request
            UserCredentials creds = new ObjectMapper().readValue(request.getInputStream(), UserCredentials.class);
            
            // 2. Create auth object (contains credentials) which will be used by auth manager
            UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                    creds.getUsername(), creds.getPassword(), Collections.emptyList());
            
            // 3. Authentication manager authenticate the user, and use UserDetialsServiceImpl::loadUserByUsername() method to load the user.
            return authManager.authenticate(authToken);
            
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    // Upon successful authentication, generate a token.
    // The 'auth' passed to successfulAuthentication() is the current authenticated user.
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
            Authentication auth) throws IOException, ServletException {
        
        Long now = System.currentTimeMillis();
        String token = Jwts.builder()
            .setSubject(auth.getName()) 
            // Convert to list of strings. 
            // This is important because it affects the way we get them back in the Gateway.
            .claim("authorities", auth.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
            .setIssuedAt(new Date(now))
            .setExpiration(new Date(now + jwtConfig.getExpiration() * 1000))  // in milliseconds
            .signWith(SignatureAlgorithm.HS512, jwtConfig.getSecret().getBytes())
            .compact();
        
        // Add token to header
        response.addHeader(jwtConfig.getHeader(), jwtConfig.getPrefix() + token);
    }
    
    // A (temporary) class just to represent the user credentials
    private static class UserCredentials {
        private String username, password;
        // getters and setters ...
    }
}

共同事務(wù)

如果您有多個服務(wù)使用的公共配置變量,枚舉類或邏輯,就像我們擁有的那樣JwtConfig。我們將其放在一個單獨的服務(wù)中,而不是復(fù)制代碼,該服務(wù)可以包含在其他服務(wù)中并作為依賴項使用。

為此,只需創(chuàng)建一個新項目(服務(wù)),將其命名為“common”,然后按照與圖像服務(wù)相同的步驟操作。所以,在pom.xml文件中

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

在里面 application.properties

spring.application.name=common-service
server.port=9200
eureka.client.service-url.default-zone=http://localhost:8761/eureka

在spring boot主應(yīng)用程序類中

package com.eureka.common;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class SpringEurekaCommonApp {
public static void main(String[] args) {
  SpringApplication.run(SpringEurekaCommonApp.class, args);
 }
}

然后,復(fù)制JwtConfig我們之前在Gateway中創(chuàng)建的公共服務(wù)類。

package com.eureka.common.security;
import org.springframework.beans.factory.annotation.Value;
public class JwtConfig {
   // ...    
}

現(xiàn)在,為了能夠JwtConfig從其他服務(wù)(如auth和gateway)調(diào)用類,我們只需要將公共服務(wù)添加pom.xml為依賴項。

<dependency>
  <groupId>com.eureka.common</groupId>
  <artifactId>spring-eureka-common</artifactId>
  <version>0.0.1-SNAPSHOT</version>
</dependency>

在我們的身份驗證和網(wǎng)關(guān)服務(wù)中......

// change these lines of code
import com.eureka.zuul.security.JwtConfig;
import com.eureka.auth.security.JwtConfig;
// to reference the class in common service instead
import com.eureka.common.security.JwtConfig;

測試我們的微服務(wù)

現(xiàn)在我們插入了身份驗證邏輯,我們可以無縫地驗證憑據(jù),發(fā)放令牌和驗證用戶身份。

所以,運行我們的Eureka服務(wù)器。然后,運行其他服務(wù):image,gallery,common,auth,最后是網(wǎng)關(guān)。

首先,讓我們嘗試在localhost:8762/gallery沒有令牌的情況下訪問圖庫服務(wù)。你應(yīng)該得到Unauthorized錯誤。

{
    "timestamp": "...",
    "status": 401,
    "error": "Unauthorized",
    "message": "No message available",
    "path": "/gallery/"
}

要獲取令牌,請將用戶憑據(jù)發(fā)送給localhost:8762/auth(我們在UserDetailsServiceImpl上面的類中硬編碼了兩個用戶),并確保Content-Type將頭中的用戶分配給application/json

現(xiàn)在,我們可以向標(biāo)頭中的令牌傳遞令牌服務(wù)請求。

如果為管理員用戶創(chuàng)建了令牌,那么您應(yīng)該能夠訪問圖庫服務(wù)的管理區(qū)域。

同樣,如果您正在運行多個gallery服務(wù)實例,每個實例都在不同的端口運行,那么請求將在它們之間平均分配。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容