首頁  >  文章  >  Java  >  怎麼使用SpringBoot+SpringSecurity+JWT實現系統認證與授權

怎麼使用SpringBoot+SpringSecurity+JWT實現系統認證與授權

WBOY
WBOY轉載
2023-05-14 20:16:041792瀏覽

1. Spring Security簡介

Spring Security是Spring的一個核心項目,它是一個功能強大且高度可自訂的認證和存取控制框架。它提供了認證和授權功能以及抵禦常見的攻擊,它已經成為保護基於spring的應用程式的事實標準。

Spring Boot提供了自動配置,引入starter依賴即可使用。
Spring Security特性摘要:

  • 使用簡單,提供Spring Boot starter依賴,極易與Spring Boot專案整合。

  • 專業,提供CSRF防護、點擊劫持防護、XSS防護等,並提供各種安全頭整合(X-XSS-Protection,X-Frame-Options等)。

  • 密碼加密存儲,支援多種加密演算法

  • #可擴展性和可自訂性極強

##OAuth3 JWT認證支援




… …

2. JWT簡介

##JWT(Json web token ),是為了在網路應用環境間傳遞聲明而執行的一種基於JSON的開放標準(RFC 7519).該token被設計為緊湊且安全的,特別適用於分佈式站點的單點登錄(SSO)場景。 JWT的聲明一般被用來在身分提供者和服務提供者間傳遞被認證的使用者身分訊息,以便於從資源伺服器取得資源,也可以增加一些額外的其它業務邏輯所必須的聲明資訊(例如,權限資訊).一旦用戶被授予token,用戶即可透過該token存取伺服器上的資源。

3. Spring Boot整合Spring Security

注意這篇文章示範使用JDK和Spring Boot的版本如下:Spring Boot:2.7.2JDK: 11

不同的Spring Boot版本配置不同,但是原理相同。

在Spring Boot專案的pom.xml檔案中加入下面的依賴:

<!-- Spring Security的Spring boot starter,引入后将自动启动Spring Security的自动配置 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 下面的依赖包含了OAuth3 JWT认证实现 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth3-resource-server</artifactId>
</dependency>
以上兩個依賴即可。

4. 設定Spring Security使用JWT認證


注意:

不同的Spring Boot版本配置不同,但是原理相同,本文使用的是Spring Boot: 2.7.2。

主要是設定HttpSecurity Bean產生SecurityFilterBean,設定如下:
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth3.jwt.JwtDecoder;
import org.springframework.security.oauth3.jwt.JwtEncoder;
import org.springframework.security.oauth3.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth3.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth3.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth3.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth3.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth3.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.web.SecurityFilterChain;

import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;

/**
 * Spring Security 配置
 *
 * @author cloudgyb
 * @since 2022/7/30 18:31
 */
@Configuration(proxyBeanMethods = false)
@EnableMethodSecurity
public class WebSecurityConfigurer {
    //使用RSA对JWT做签名,所以这里需要一对秘钥。
    //秘钥文件的路径在application.yml文件中做了配置(具体配置在下面)。
    @Value("${jwt.public.key}")
    private RSAPublicKey key; 
    @Value("${jwt.private.key}")
    private RSAPrivateKey priv;

     /**
      * 构建SecurityFilterChain bean
      */
    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        //"/login"是系统的登录接口,所以需要匿名可访问
        http.authorizeRequests().antMatchers("/login").anonymous();
        //其他请求都需认证后才能访问
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                
                //采用JWT认证无需session保持,所以禁用掉session管理器
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                //login接口可能来自其他站点,所以对login不做csrf防护
                .csrf((csrf) -> csrf.ignoringAntMatchers("/login"))
                //配置认证方式为JWT,并且配置了一个JWT认证装换器,用于去掉解析权限时的SCOOP_前缀
                .oauth3ResourceServer().jwt().jwtAuthenticationConverter(
                        JwtAuthenticationConverter()
                );
        //配置认证失败或者无权限时的处理器
        http.exceptionHandling((exceptions) -> exceptions
                .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
        );
         //根据配置生成SecurityFilterChain对象
        return http.build();
    }


    /**
     * JWT解码器,用于认证时的JWT解码 
     */
    @Bean
    JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withPublicKey(this.key).build();
    }
    /**
     * JWT编码器,生成JWT
     */
    @Bean
    JwtEncoder jwtEncoder() {
        JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build();
        JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
        return new NimbusJwtEncoder(jwks);
    }
    
    /**
     * JWT认证解码时,去掉Spring Security对权限附带的默认前缀SCOOP_
     */
    @Bean
    JwtAuthenticationConverter JwtAuthenticationConverter() {
        final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
        final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }
}

application.yml

jwt:
  private.key: classpath:app.key
  public.key: classpath:app.pub
上邊的設定需要在Spring Boot專案的Resource目錄下產生一對RSA秘鑰。

可以使用下面的網站進行產生:http://tools.jb51.net/password/rsa_encode/,
注意: 金鑰格式使用 PKCS#8,私鑰密碼為空。

還有一點要說明,我在程式碼中使用了Spring Boot的值注入:

@Value("${jwt.public.key}")
 private RSAPublicKey key; 
@Value("${jwt.private.key}")
private RSAPrivateKey priv;

有沒有很好奇Spring Boot是如何將yaml檔案中的字串對應的檔案轉換為RSAPublicKey和RSAPrivateKey ?

其實是Spring Security幫我們做了處理,在Spring Security中幫我們實作了一個轉換器ResourceKeyConverterAdapter,可以閱讀相關原始碼來更深入的了解。

至此我們的專案已經支援JWT認證了。

但是使用者需要在請求頭Authorization中攜帶合法的JWT才能通過認證,進而存取伺服器資源,那麼如何給使用者頒發一個合法的JWT呢?

很簡單,可以提供一個登入接口,讓使用者輸入使用者名稱和密碼,匹配成功後頒發令牌即可。

其實並不是必須這樣做,還有其他方式,例如我們調用第三方接口,我們經常的做法是先去第三方申請,申請通過後我們就可以得到一個令牌。這個過程和上面的登入通過後頒發一個令牌是一樣的,都是透過合法的途徑獲得一個令牌!


5. 實作登入介面

怎麼使用SpringBoot+SpringSecurity+JWT實現系統認證與授權登入介面只有一個目的,就是要頒發給合法使用者令牌!

登入API介面:

@RestController
public class SysLoginController {
    private final SysLoginService sysLoginService;

    public SysLoginController(SysLoginService sysLoginService) {
        this.sysLoginService = sysLoginService;
    }

    @PostMapping("/login")
    public String login(@RequestBody LoginInfo loginInfo) {
        return sysLoginService.login(loginInfo);
    }
}

登入邏輯實作:

@Service
public class SysLoginService {
    private final JwtEncoder jwtEncoder;
    private final SpringSecurityUserDetailsService springSecurityUserDetailsService;

    public SysLoginService(JwtEncoder jwtEncoder, SpringSecurityUserDetailsService springSecurityUserDetailsService) {
        this.jwtEncoder = jwtEncoder;
        this.springSecurityUserDetailsService = springSecurityUserDetailsService;
    }

    public String login(LoginInfo loginInfo) {
        //从用户信息存储库中获取用户信息
        final UserDetails userDetails = springSecurityUserDetailsService.loadUserByUsername(loginInfo.getUsername());
        final String password = userDetails.getPassword();
        //匹配密码,匹配成功生成JWT令牌
        if (password.equals(loginInfo.getPassword())) {
            return generateToken(userDetails);
        }
        //密码不匹配,抛出异常,Spring Security发现抛出该异常后会将http响应状态码设置为401 unauthorized
        throw new BadCredentialsException("密码错误!");
    }

    private String generateToken(UserDetails userDetails) {
        Instant now = Instant.now();
        //JWT过期时间为36000秒,也就是600分钟,10小时
        long expiry = 36000L;
        String scope = userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(" "));
         //将用户权限信息使用空格分割拼为字符串,放到JWT的payload的scope字段中,注意不要改变scope这个属性,这是Spring Security OAuth3 JWT默认处理方式,在JWT解码时需要读取该字段,转为用户的权限信息!
        JwtClaimsSet claims = JwtClaimsSet.builder()
                .issuer("self")
                .issuedAt(now)
                .expiresAt(now.plusSeconds(expiry))
                .subject(userDetails.getUsername())
                .claim("scope", scope)
                .build();
        return this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
    }
}
怎麼使用SpringBoot+SpringSecurity+JWT實現系統認證與授權其他非核心程式碼這裡就不貼出來了,我將程式碼放到github上了,具體可以轉到https ://github.com/cloudgyb/spring-security-study-jwt。

6. 測試

使用postman測試一下:
使用錯誤的密碼,會回傳401 Unauthorized的狀態碼,表示我們認證失敗!


使用正確的使用者名稱和密碼:怎麼使用SpringBoot+SpringSecurity+JWT實現系統認證與授權

#########傳回了JWT令牌。 ######此時客戶端拿到了合法的令牌,接下來就可以存取伺服器上有權存取的資源了。 ###我寫了一個測試介面:###
@RestController
public class HelloController {

    @GetMapping("/")
    @PreAuthorize("hasAuthority(&#39;test&#39;)")
    public String hello(Authentication authentication) {
        return "Hello, " + authentication.getName() + "!";
    }
}
###該介面需要使用者擁有"test"的權限,但登入使用者沒有該權限(只有一個app的權限),此時呼叫該介面:## #首先將上一步驟登入所獲得的令牌貼到token中:############

我们发送请求得到了403 Forbidden的响应,意思就是我们没有访问权限,此时我们将接口权限改为“app”:

@RestController
public class HelloController {

    @GetMapping("/")
    @PreAuthorize("hasAuthority(&#39;app&#39;)")
    public String hello(Authentication authentication) {
        return "Hello, " + authentication.getName() + "!";
    }
}

重启项目。再次发起请求:

怎麼使用SpringBoot+SpringSecurity+JWT實現系統認證與授權

以上是怎麼使用SpringBoot+SpringSecurity+JWT實現系統認證與授權的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除